Ctrl K
ComunidadGuías…

DOCUMENTATION.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Sharely — Technische Dokumentation

> **Version:** 1.0.0 · **Lizenz:** MIT · **Node.js:** ≥ 22

---

## Inhaltsverzeichnis

1. [Projektübersicht](#1-projektübersicht)
2. [Architektur](#2-architektur)
3. [Tech-Stack](#3-tech-stack)
4. [Verzeichnisstruktur](#4-verzeichnisstruktur)
5. [Konfiguration & Umgebungsvariablen](#5-konfiguration--umgebungsvariablen)
6. [Datenmodelle](#6-datenmodelle)
7. [REST API](#7-rest-api)
8. [WebSocket API](#8-websocket-api)
9. [Datei-Serving-Routen](#9-datei-serving-routen)
10. [Authentifizierung & Sicherheit](#10-authentifizierung--sicherheit)
11. [Upload-System](#11-upload-system)
12. [Thumbnail-Generierung](#12-thumbnail-generierung)
13. [E-Mail & SMTP](#13-e-mail--smtp)
14. [Internationalisierung](#14-internationalisierung)
15. [Frontend (React SPA)](#15-frontend-react-spa)
16. [Admin-Dashboard](#16-admin-dashboard)
17. [GDPR / Datenschutz](#17-gdpr--datenschutz)
18. [Hintergrund-Jobs](#18-hintergrund-jobs)
19. [Deployment](#19-deployment)
20. [Entwicklungsumgebung](#20-entwicklungsumgebung)
21. [Migrationen & Skripte](#21-migrationen--skripte)
22. [End-to-End-Tests](#22-end-to-end-tests)

---

## 1. Projektübersicht

Sharely ist eine **selbst gehostete Dateifreigabe-Plattform** mit einer sauberen Web-Oberfläche, ShareX-Integration und API-Zugang. Nutzer laden Screenshots, Dateien und Medien hoch und teilen sie sofort über Kurz-Links.

### Kernfunktionen

| Feature | Beschreibung |
|---|---|
| Web-Upload | Drag-and-Drop, bis zu 500 Dateien gleichzeitig |
| Chunked Upload | Dateien bis 2 GB via parallelem Multi-Part-Upload |
| ShareX-Integration | `.sxcu`-Konfigurationsdatei per Knopfdruck herunterladbar |
| API-Upload | Bearer-Token-Authentifizierung, kompatibel mit curl/wget |
| Datei-Viewer | Bilder zoomen, Videos/Audio streamen (HTTP Range), PDFs inline, Code syntax-highlighted |
| Einbettungsmodi | *embed* (OG/Twitter-Card-HTML) oder *raw* (Direkt-Redirect) |
| Thumbnails | Automatische JPEG-Vorschaubilder für Videos (ffmpeg) und PDFs (ghostscript) |
| Collections | Gruppensammlungen von Dateien mit optionalem Passwort und Ablaufdatum |
| Share-Links | Per-Datei-Links mit Passwort, Ablauf und Download-Limit |
| Echtzeit-UI | WebSocket-basierte Live-Updates (Upload, Delete, View-Counter, Admin-Stats) |
| Mehrsprachigkeit | 8 Sprachen: EN, DE, FR, ES, IT, PT, JA, ZH |
| Admin-Dashboard | Statistiken, Nutzerverwaltung, Dateiverwaltung, Audit-Log (CSV-Export) |
| GDPR-Compliance | Datenschutz-Features nach EU-DSGVO (Art. 17, 20, 32 u. a.) |
| XBackBone-Import | Migration bestehender XBackBone-Installationen |
| Docker-ready | `docker compose up -d` startet die vollständige Umgebung |

---

## 2. Architektur

```
┌─────────────────────────────────────────────────────────────┐
│                        Browser / Client                      │
│            React 18 SPA  ·  Vite  ·  Tailwind CSS           │
│   ┌──────────────────────────────────────────────────────┐   │
│   │  HTTP REST (/api/*)            WebSocket (/ws)       │   │
│   └──────────────────────────────────────────────────────┘   │
└───────────────────────────┬────────────────────┬─────────────┘
                            │ HTTP               │ WS
┌───────────────────────────▼────────────────────▼─────────────┐
│                    Express.js (app.js)                        │
│                                                               │
│  ┌──────────────┐  ┌───────────┐  ┌──────────┐  ┌────────┐  │
│  │  /api/auth   │  │  /api/*   │  │   /f/*   │  │  /s/*  │  │
│  │  /api/install│  │  routes   │  │  files   │  │ shares │  │
│  └──────────────┘  └───────────┘  └──────────┘  └────────┘  │
│                                                               │
│  Middleware: session · rate-limit · CSRF · CSP · auth        │
│                                                               │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌────────────┐  │
│  │  multer  │  │  ws.js   │  │  mailer  │  │ retention  │  │
│  │  upload  │  │ WebSocket│  │ nodemailer│  │  cleanup   │  │
│  └──────────┘  └──────────┘  └──────────┘  └────────────┘  │
└───────────────────────────┬───────────────────────────────────┘
                            │
┌───────────────────────────▼───────────────────────────────────┐
│                      MongoDB (Mongoose)                        │
│  User · File · Collection · ShareLink · SiteSettings          │
│  AuditLog                                                      │
└───────────────────────────────────────────────────────────────┘
                            │
              ┌─────────────▼───────────────┐
              │     Dateisystem (uploads/)   │
              │  {folderName}/  ·  .chunks/  │
              │  .thumbnails/   ·  .avatars/ │
              └─────────────────────────────┘
```

### Datenfluss: Standard-Upload

```
Browser → POST /api/web-upload (multipart/form-data)
       → multer: Datei in uploads/{folderName}/
       → normalizeMime(): MIME-Typ auf genau einen "typ/subtyp" reduzieren
       → stripIfEnabled(): Bild-Metadaten entfernen (falls aktiviert)
       → sealUpload(): mit ENCRYPTION_KEY verschlüsseln (sonst unverändert)
       → File.createUnique() → MongoDB
       → startThumbnail() (async, non-blocking; danach Klartext-Kopie löschen)
       → logAudit()
       → broadcast('file:uploaded') via WebSocket
       ← JSON: { files: [...] }
```

### Datenfluss: ShareX-Upload

```
ShareX → POST /upload (token im Formular-Body)
       → multer: Datei temporär in uploads/
       → requireApiKey: Token-Lookup → User (bei Fehler wird die Datei sofort gelöscht)
       → fs.renameSync → uploads/{folderName}/
       → normalizeMime() · sealUpload() · File.createUnique() → MongoDB
       → startThumbnail()
       ← JSON: { url, delete_url }
```

---

## 3. Tech-Stack

| Schicht | Technologie | Version |
|---|---|---|
| **Laufzeit** | Node.js | ≥ 22 |
| **Backend-Framework** | Express.js | 5.x |
| **Datenbank** | MongoDB + Mongoose | Mongo 9, Mongoose 9.x |
| **Sessions** | express-session + connect-mongo | — |
| **Echtzeit** | WebSocket (`ws`) | 8.x |
| **Datei-Upload** | Multer | 2.x |
| **Datei-Verschlüsselung** | AES-256-GCM, HKDF-SHA256 (Node Crypto) | — |
| **E-Mail** | Nodemailer | 10.x |
| **Passwort-Hashing** | bcryptjs | 3.x (12 Rounds) |
| **API-Key-Hashing** | SHA-256 (Node Crypto) | — |
| **Rate-Limiting** | express-rate-limit | 8.x |
| **XBackBone-Import** | sql.js | 1.x |
| **Frontend-Framework** | React 19 | 19.x |
| **Routing (Frontend)** | React Router v7 | 7.x |
| **Build-Tool** | Vite | 8.x |
| **Styling** | Tailwind CSS + Radix UI | 4.x |
| **UI-Komponenten** | shadcn/ui (Radix primitives) | — |
| **Icons** | FontAwesome | 7.x |
| **i18n** | i18next + react-i18next | — |
| **Syntax-Highlighting** | highlight.js | 11.x |
| **Container** | Docker + Docker Compose | — |
| **Tests** | Playwright (E2E) | 1.64.x |

---

## 4. Verzeichnisstruktur

```
sharely/
├── app.js                          # Express-Einstiegspunkt, Startup-Sequenz
├── package.json
├── .env.example                    # Vorlage für alle Umgebungsvariablen
├── Dockerfile
├── docker-compose.yml
│
├── src/
│   ├── config/
│   │   └── db.js                   # MongoDB-Verbindung (Mongoose)
│   ├── middleware/
│   │   ├── auth.js                 # requireLogin / requireAdmin / requireApiKey / establishSession
│   │   ├── lookupLimiter.js        # Gemeinsames Rate-Limit für fehlgeschlagene ID-Lookups
│   │   └── upload.js               # Multer-Konfiguration, Blockliste
│   ├── models/
│   │   ├── AuditLog.js             # Audit-Events (TTL 90 Tage)
│   │   ├── Collection.js           # Dateisammlungen
│   │   ├── File.js                 # Datei-Metadaten
│   │   ├── Paste.js                # Text-Pastes
│   │   ├── ShareLink.js            # Per-Datei-Share-Links
│   │   ├── SiteSettings.js         # Singleton: Betreiber-Einstellungen
│   │   └── User.js                 # Nutzerkonten + API-Keys
│   ├── routes/
│   │   ├── api.js                  # Haupt-API (Upload, Gallery, Admin, ...)
│   │   ├── auth.js                 # Login / Register / Password-Reset
│   │   ├── docs.js                 # Öffentliche Dokumentationsseite (/docs)
│   │   ├── files.js                # Datei-Serving, OG-Embeds, Range-Requests
│   │   ├── import.js               # XBackBone-Migration
│   │   ├── install.js              # Erstinstallations-Endpoint (Setup-Token)
│   │   ├── pastes.js               # Paste-API
│   │   └── shares.js               # Share-Link-Datei-Serving
│   ├── jobs/
│   │   └── retentionCleanup.js     # Tägliches Löschen abgelaufener Dateien und Upload-Reste
│   ├── migrations/
│   │   ├── migrateApiKeyHashes.js  # Einmalig: Plaintexts → SHA-256-Hashes
│   │   └── migrateUserFolders.js   # Einmalig: Dateien in Nutzer-Ordner verschieben
│   ├── utils/
│   │   ├── asyncRouter.js          # Express-Router, der async-Fehler an den Error-Handler gibt
│   │   ├── audit.js                # logAudit()-Hilfsfunktion
│   │   ├── deleteUserData.js       # Vollständige Löschung aller Daten eines Kontos
│   │   ├── fileCrypto.js           # Verschlüsselung gespeicherter Dateien (AES-256-GCM)
│   │   ├── generateThumbnail.js    # Thumbnail erzeugen (lokal oder über thumbnailer-Container)
│   │   ├── thumbnailQueue.js       # Datei-Warteschlange zum thumbnailer-Container
│   │   ├── thumbnailTools.js       # ffmpeg / ghostscript Aufrufe
│   │   ├── mailer.js               # Nodemailer-Wrapper + i18n-E-Mail-Templates
│   │   ├── mime.js                 # MIME-Normalisierung, Whitelist für Inline-Auslieferung
│   │   ├── sanitizeFilename.js     # Dateiname bereinigen
│   │   ├── setupToken.js           # Setup-Token der Erstinstallation
│   │   ├── storeUpload.js          # Upload verschlüsseln, Thumbnail aus Klartext-Kopie
│   │   └── uploadPath.js           # Pfadauflösung mit Path-Traversal-Schutz
│   ├── thumbnailWorker.js          # Einstieg des thumbnailer-Containers
│   └── ws.js                       # WebSocket-Server + Action-Dispatcher
│
├── client/                         # React-Frontend
│   ├── index.html
│   ├── package.json
│   ├── vite.config.js
│   ├── postcss.config.js
│   └── src/
│       ├── main.jsx                # React-Einstiegspunkt
│       ├── App.jsx                 # Router-Konfiguration
│       ├── index.css               # Globale Styles
│       ├── context/
│       │   └── AuthContext.jsx     # Globaler Auth-State
│       ├── hooks/
│       │   ├── use-toast.js        # Toast-Notification-Hook
│       │   └── useWebSocket.js     # WS-Verbindung + Event-Handler
│       ├── components/
│       │   ├── Layout.jsx          # App-Shell (Navbar, Sidebar)
│       │   ├── ProtectedRoute.jsx  # Auth-Guard
│       │   ├── ShareLinkDialog.jsx # Share-Link-Erstell-Dialog
│       │   ├── AddToCollectionDialog.jsx
│       │   ├── CookieBanner.jsx
│       │   ├── LanguageSelector.jsx
│       │   ├── RequireEmailDialog.jsx
│       │   ├── UserAvatar.jsx
│       │   └── ui/                 # shadcn/ui Basis-Komponenten
│       ├── pages/
│       │   ├── Upload.jsx          # Upload-Seite
│       │   ├── Gallery.jsx         # Datei-Galerie
│       │   ├── FileView.jsx        # Datei-Detailansicht
│       │   ├── Collections.jsx     # Sammlungsübersicht
│       │   ├── CollectionView.jsx  # Einzelne Sammlung
│       │   ├── ShareView.jsx       # Öffentliche Share-Link-Seite
│       │   ├── Settings.jsx        # Nutzer-Einstellungen
│       │   ├── Login.jsx
│       │   ├── Register.jsx
│       │   ├── ForgotPassword.jsx
│       │   ├── ResetPassword.jsx
│       │   ├── Install.jsx         # Erstinstallation
│       │   ├── PrivacyPolicy.jsx
│       │   ├── TermsOfService.jsx
│       │   └── admin/
│       │       ├── Dashboard.jsx   # Admin-Startseite
│       │       ├── Users.jsx       # Nutzerverwaltung
│       │       ├── Files.jsx       # Dateiverwaltung
│       │       ├── AuditLog.jsx    # Audit-Log-Ansicht
│       │       ├── SiteSettings.jsx# Betreiber-Einstellungen
│       │       └── Import.jsx      # XBackBone-Import
│       ├── i18n/
│       │   ├── index.js            # i18next-Konfiguration
│       │   └── locales/
│       │       ├── de.json
│       │       ├── en.json
│       │       ├── es.json
│       │       ├── fr.json
│       │       ├── it.json
│       │       ├── ja.json
│       │       ├── pt.json
│       │       └── zh.json
│       └── lib/
│           └── utils.js            # Tailwind-Helfer (cn())
│
├── scripts/
│   ├── setup-db.js
│   ├── mongo-init.js               # MongoDB-Initialisierungsskript
│   ├── migrate-uploads-to-user-folders.js
│   ├── generate-missing-thumbnails.js
│   └── encrypt-existing-files.js   # Bestehende Dateien nachträglich verschlüsseln
│
├── e2e/                            # Playwright-Tests
│   ├── admin.spec.js
│   ├── bulk-actions-fixes.spec.js
│   ├── gallery.spec.js
│   ├── sharelink.spec.js
│   ├── tags.spec.js
│   ├── upload.spec.js
│   ├── helpers.js
│   └── global-setup.js
│
├── uploads/                        # Datei-Uploads (Laufzeit)
│   ├── .thumbnails/
│   ├── .avatars/
│   └── .chunks/                    # Temporäre Chunks
│
└── docs/assets/                    # Screenshots und Logo
```

---

## 5. Konfiguration & Umgebungsvariablen

Alle Variablen werden aus `.env` geladen (via `dotenv`). Die Datei `.env.example` enthält die vollständige Vorlage.

### Pflichtfelder

| Variable | Beschreibung |
|---|---|
| `SESSION_SECRET` | Geheimnis für Session-Verschlüsselung — langer Zufallsstring, z. B. `openssl rand -hex 32` |
| `MONGO_ROOT_PASSWORD` | MongoDB-Root-Passwort (nur für Docker Compose benötigt) |
| `MONGO_APP_PASSWORD` | MongoDB-App-Nutzer-Passwort |

### Alle Umgebungsvariablen

| Variable | Standard | Beschreibung |
|---|---|---|
| `PORT` | `3000` | TCP-Port des HTTP-Servers |
| `BIND_ADDRESS` | `127.0.0.1` | Host-Adresse, auf der Docker den Port veröffentlicht. Hinter Reverse Proxy auf Loopback lassen; `0.0.0.0` nur hinter externer Firewall oder ohne Proxy (siehe „Cloudflare Tunnel / Proxy im Container“) |
| `TRUST_PROXY` | `1` | Anzahl vertrauenswürdiger Proxy-Hops (z. B. `2` für Cloudflare → nginx); muss zum Setup passen, sonst sind Client-IPs und Rate-Limits falsch |
| `ENCRYPTION_KEY` | — | 32-Byte-Schlüssel (`openssl rand -base64 32`). Ist er gesetzt, werden neue Uploads und Thumbnails verschlüsselt gespeichert (siehe „Verschlüsselung gespeicherter Dateien“). **Geht er verloren, sind verschlüsselte Dateien unlesbar** |
| `INSTALL_TOKEN` | _(zufällig, wird ins Log geschrieben)_ | Token, das für die Erstinstallation nötig ist |
| `MONGODB_URI` | _(aus Docker Compose konstruiert)_ | Vollständige MongoDB-Verbindungs-URI |
| `MONGO_ROOT_PASSWORD` | — | MongoDB-Root-Passwort |
| `MONGO_APP_USER` | `appuser` | MongoDB-App-Nutzername |
| `MONGO_APP_PASSWORD` | — | MongoDB-App-Nutzer-Passwort |
| `MONGO_DB_NAME` | `sharely` | MongoDB-Datenbankname |
| `SESSION_SECRET` | — | **Pflicht** — Session-Verschlüsselungsgeheimnis |
| `BASE_URL` | `http://localhost:3000` | Öffentliche Basis-URL für generierte Share-Links (kein abschließendes `/`) |
| `SITE_NAME` | `sharely` | Sitename in Open Graph Embeds |
| `MAX_FILE_SIZE_MB` | `100` | Maximale Dateigröße für Standard-Uploads in MB (Chunked Uploads bis 2 GB unabhängig davon) |
| `ALLOW_REGISTRATION` | `true` | `false` deaktiviert öffentliche Registrierung |
| `SMTP_HOST` | — | SMTP-Server-Hostname; leer lassen = E-Mail-Features deaktiviert |
| `SMTP_PORT` | `587` | SMTP-Port |
| `SMTP_SECURE` | `false` | `true` für implizites TLS (Port 465), `false` für STARTTLS |
| `SMTP_USER` | — | SMTP-Benutzername |
| `SMTP_PASS` | — | SMTP-Passwort |
| `SMTP_FROM` | _(SMTP_USER)_ | Absenderadresse in ausgehenden E-Mails |
| `UPLOAD_DIR` | `./uploads` | Absoluter Pfad zum Upload-Verzeichnis |
| `NODE_ENV` | — | Optional. Sichere Cookies hängen nicht davon ab: Sie werden automatisch gesetzt, sobald der Request per HTTPS ankommt (`X-Forwarded-Proto` vom Proxy) |
| `UPDATE_ENABLED` | `false` | Aktiviert das Self-Update per Knopfdruck im Admin-Panel |
| `UPDATE_CHANNEL` | `release` | Update-Erkennung: `release` (neuestes GitHub-Release-Tag) oder `branch` (neue Commits auf `UPDATE_BRANCH`) |
| `UPDATE_BRANCH` | `main` | Branch, den der Updater zieht — und im `branch`-Modus zum Vergleich heranzieht |
| `UPDATE_REPO` | `Christianoooooo/sharely` | GitHub-Repo, das auf neue Releases geprüft wird |
| `COMPOSE_PROJECT_NAME` | `sharely` | Compose-Projektname; muss zwischen Host und Updater identisch sein |

### Automatische Updates

Das Admin-Panel (**Admin → Website-Einstellungen → Version & Updates**) zeigt stets die installierte Version und ob ein Update verfügbar ist.

**Woran „Update verfügbar" festgemacht wird**, steuert `UPDATE_CHANNEL`:

- `release` (Standard) — die installierte Version wird mit dem neuesten **GitHub-Release-Tag** verglichen. Passend, wenn pro Version ein Release erstellt wird.
- `branch` — der installierte **Commit** wird mit der Spitze von `UPDATE_BRANCH` verglichen. Ein Update ist verfügbar, sobald neue Commits auf dem Branch liegen — ohne dass ein Release nötig ist. Installierte Version/Commit meldet der Updater-Container, daher braucht dieser Modus den laufenden Updater.

Optional lässt sich ein Update per Knopfdruck einspielen. Dies übernimmt ein eigener `updater`-Container — die **einzige** Komponente mit Zugriff auf den Docker-Socket. Die Web-App berührt den Socket nie: sie legt nur eine Anforderungsdatei in ein gemeinsames Volume, der Updater führt dann `git pull` + `docker compose build app thumbnailer` + `docker compose up -d app thumbnailer` aus.

> ⚠️ **Sicherheitshinweis:** Der Updater mountet den Docker-Socket, was Root-Rechten auf dem Host entspricht. Nur aktivieren, wenn dieser Kompromiss bewusst akzeptiert wird. Bei einer kompromittierten Web-App kann höchstens ein Rebuild aus dem konfigurierten Branch ausgelöst werden — keine beliebigen Host-Befehle. Dafür gehört das gemeinsame Control-Verzeichnis `root` und hat das Sticky-Bit (`1777`): Die App darf nur `update.request` anlegen und `update.state` schreiben, aber keine Dateien des Updaters ersetzen oder durch Symlinks austauschen.

Voraussetzungen: Deployment aus einem **Git-Checkout**, `UPDATE_ENABLED=true` in der `.env`, unveränderter `COMPOSE_PROJECT_NAME`, und Start inklusive Updater via `docker compose --profile updater up -d --build`. Der Updater liegt hinter dem Compose-Profil `updater` — ein normales `docker compose up` startet den privilegierten Container also nie. Beim Einspielen wird der App-Container neu gebaut und ist kurz nicht erreichbar; die Seite zeigt den Fortschritt und verbindet sich automatisch neu. Mit `UPDATE_ENABLED=false` (und ohne Profil) bleibt das Panel reine Benachrichtigung.

Der Updater baut per `--no-deps` ausschließlich den App- und den Thumbnail-Container neu — die Datenbank wird nie mit-recreated. Er aktualisiert jedoch **sich selbst nicht**: Nach Änderungen unter `updater/` ist einmalig `docker compose --profile updater up -d --build updater` nötig.

---

## 6. Datenmodelle

### User (`src/models/User.js`)

```
{
  username:                    String (3–32, unique, alphanumerisch + _-)
  password:                    String (bcrypt, 12 Rounds)
  role:                        'admin' | 'user'
  apiKey:                      String (Legacy, nach Migration leer)
  apiKeyHash:                  String (SHA-256, unique, sparse)
  apiKeyPrefix:                String (erste 8 Zeichen des Klartexts)
  folderName:                  String (unique, sparse, max 64)
  avatarExt:                   String | null (.jpg/.png/.gif/.webp)
  embedMode:                   'embed' | 'raw'
  isActive:                    Boolean
  sessionVersion:              Number (wird bei Passwort-Änderung/-Reset erhöht, macht andere Sessions ungültig)
  email:                       String (lowercase, unique, sparse)
  emailVerified:               Boolean
  emailVerificationToken:      String | null (SHA-256-Hash des Klartexts)
  emailVerificationExpires:    Date | null
  passwordResetToken:          String | null (SHA-256-Hash)
  passwordResetExpires:        Date | null
  language:                    'en'|'de'|'fr'|'es'|'it'|'pt'|'ja'|'zh'
  predefinedTags:              [String] (max 100 Tags × 50 Zeichen)
  createdAt:                   Date
}
```

**Wichtige Methoden:**
- `user.comparePassword(candidate)` — bcrypt-Vergleich
- `user.regenerateApiKey()` — generiert neuen Plaintext, speichert Hash, gibt Plaintext zurück (einmalig sichtbar)
- `User.findByApiKey(plaintext)` — sucht nach SHA-256-Hash, nur aktive Nutzer

### File (`src/models/File.js`)

```
{
  shortId:       String (24 Hex-Zeichen, zufällig, unique)
  deleteToken:   String (32 Hex-Zeichen, unique; select: false, nie in API-Antworten)
  originalName:  String (sanitisiert)
  storedName:    String (relativer Pfad: "folderName/8hex.ext")
  mimeType:      String (normalisiert auf genau einen "typ/subtyp")
  encrypted:     Boolean (Datei liegt im Format von src/utils/fileCrypto.js vor)
  size:          Number (Bytes, Klartextgröße)
  uploader:      ObjectId → User
  views:         Number
  expiresAt:     Date | null (Selbstzerstörung nach Datum)
  maxDownloads:  Number | null (Selbstzerstörung nach N Downloads)
  downloadCount: Number
  tags:          [String] (max 20 × 50 Zeichen)
  description:   String (max 1000)
  createdAt:     Date
}
```

**Virtuals:**
- `sizeHuman` — lesbare Größe (B / KB / MB / GB)
- `displayType` — Klassifikation: `image|video|audio|pdf|code|text|file`

**Short-ID-Algorithmus:**
```
shortId = randomBytes(12).hex()   // 24 hex chars; older IDs keep their 8-char format
```

### Collection (`src/models/Collection.js`)

```
{
  shortId:     String (24 Hex, unique)
  name:        String (max 100)
  description: String (max 500)
  owner:       ObjectId → User
  files:       [ObjectId → File]
  password:    String | null (bcrypt)
  expiresAt:   Date | null
  createdAt:   Date
}
```

> Abgelaufene Collections werden nach 7 weiteren Tagen durch MongoDB-TTL-Index automatisch gelöscht.

### ShareLink (`src/models/ShareLink.js`)

```
{
  token:         String (32 Hex, unique)
  file:          ObjectId → File
  createdBy:     ObjectId → User
  label:         String (max 100)
  password:      String | null (bcrypt)
  expiresAt:     Date | null
  downloadLimit: Number (-1 = unbegrenzt)
  downloadCount: Number
  createdAt:     Date
}
```

> Wie Collections: 7 Tage Karenzzeit nach Ablauf via TTL-Index.

### SiteSettings (`src/models/SiteSettings.js`)

Singleton-Dokument (`_id: 'singleton'`):

```
{
  operatorName:        String
  operatorAddress:     String
  operatorEmail:       String
  cloudflareAnalytics: Boolean
  fileRetentionDays:   Number (0 = deaktiviert)
  encryptionAtRest:    Boolean
  sessionDurationDays: Number (Standard: 7)
  allowRegistration:   Boolean
}
```

### AuditLog (`src/models/AuditLog.js`)

```
{
  timestamp: Date (TTL-Index: 90 Tage)
  userId:    ObjectId → User | null
  username:  String | null
  action:    String
  ip:        String | null
  meta:      Mixed (action-spezifische Metadaten)
}
```

---

## 7. REST API

### Basis-URL: `/api`

Alle JSON-Endpunkte geben `Content-Type: application/json` zurück. Fehler: `{ "error": "Nachricht" }`.

---

### Installation (`/api/install`)

| Methode | Pfad | Auth | Beschreibung |
|---|---|---|---|
| `GET` | `/status` | — | `{ installed: Boolean }` (ob bereits Nutzer existieren) |
| `POST` | `/setup` | Setup-Token | Admin-Konto und Betreiber-Einstellungen anlegen; nur solange kein Nutzer existiert |

Das Setup-Token steht beim Start im Log (`[install] Not installed yet. Setup token: …`) oder wird über `INSTALL_TOKEN` vorgegeben. Ohne passendes `setupToken` im Body antwortet `/setup` mit `403`.

---

### Authentifizierung (`/api/auth`)

| Methode | Pfad | Auth | Beschreibung |
|---|---|---|---|
| `GET` | `/me` | Session | Eingeloggter Nutzer |
| `POST` | `/login` | — | Einloggen (rate-limited: 10/15min; vergibt eine neue Session-ID) |
| `POST` | `/register` | — | Registrieren (rate-limited; der erste Admin entsteht nur über die Installation) |
| `POST` | `/logout` | — | Ausloggen |
| `GET` | `/smtp-enabled` | — | Prüft ob SMTP konfiguriert ist |
| `GET` | `/verify-email?token=` | — | E-Mail-Adresse bestätigen |
| `GET` | `/verify-reset-token?token=` | — | Reset-Token prüfen |
| `POST` | `/forgot-password` | — | Passwort-Reset-E-Mail senden (rate-limited: 5/Std.) |
| `POST` | `/reset-password` | — | Neues Passwort setzen (beendet alle bestehenden Sessions) |

**Login-Request:**
```json
POST /api/auth/login
{ "username": "max", "password": "meinPasswort123" }
```

**Login-Response:**
```json
{
  "user": {
    "id": "...", "username": "max", "role": "user",
    "avatarUrl": null, "email": "max@example.com",
    "emailVerified": true, "language": "de"
  }
}
```

---

### Datei-Upload

| Methode | Pfad | Auth | Beschreibung |
|---|---|---|---|
| `POST` | `/upload` | API-Key | ShareX-Upload (Legacy-Endpoint in `app.js`) |
| `POST` | `/api/upload` | API-Key | ShareX/API-Upload (Feld: `file`) |
| `POST` | `/api/web-upload` | Session | Web-Upload (Feld: `files[]`, max 500) |
| `POST` | `/api/chunk/init` | Session | Chunked Upload initialisieren |
| `POST` | `/api/chunk/:uploadId` | Session | Einen Chunk hochladen (Feld: `chunk`) |
| `POST` | `/api/chunk/:uploadId/complete` | Session | Chunks zusammenführen |
| `DELETE` | `/api/chunk/:uploadId` | Session | Upload abbrechen & aufräumen |

**API-Upload-Response:**
```json
{
  "url": "https://example.com/f/a1b2c3d4",
  "raw": "https://example.com/f/a1b2c3d4/raw",
  "delete_url": "https://example.com/api/delete/a1b2c3d4",
  "short_id": "a1b2c3d4",
  "filename": "screenshot.png",
  "size": 102400
}
```

#### Chunked-Upload-Flow

1. `POST /api/chunk/init` → `{ uploadId: "32hex" }`  
   Quota-Prüfung gegen `totalSize` plus die angekündigten Größen aller offenen Chunk-Uploads des Nutzers
2. `POST /api/chunk/:uploadId` (Body: `chunkIndex=N`, File: `chunk`) → `{ received: N }`  
   Parallel mit 3–5 gleichzeitigen Chunks; `413`, sobald die empfangenen Bytes `totalSize` überschreiten
3. `POST /api/chunk/:uploadId/complete` → `{ files: [fileObject] }`  
   `400`, wenn die zusammengesetzte Datei nicht exakt `totalSize` groß ist

---

### Dateiverwaltung

| Methode | Pfad | Auth | Beschreibung |
|---|---|---|---|
| `GET` | `/api/gallery` | Session | Eigene Dateien (Admin: alle), paginiert (24/Seite), Filter: `q`, `type`, `tag`, `page` |
| `GET` | `/api/file/:shortId` | — | Datei-Metadaten ohne `deleteToken` (inkrementiert View-Counter) |
| `PATCH` | `/api/file/:shortId` | Session | Tags/Name aktualisieren |
| `DELETE` | `/api/file/:shortId` | Session | Datei löschen |
| `DELETE` | `/api/delete/:shortId` | API-Key | Datei löschen (API-Key-Auth) |
| `POST` | `/api/files/bulk` | Session | Bulk-Aktionen: `delete`, `tag`, `removeTag`, `addToCollection`, `moveToCollection` |
| `GET` | `/api/tags` | Session | Alle Tag-Vorschläge des Nutzers |

**Gallery-Query-Parameter:**
- `q` — Suche im Dateinamen (regex-sicher)
- `type` — `all|image|video|audio|pdf|code`
- `tag` — Exakter Tag-Filter
- `page` — Seite (Standard: 1)

---

### Nutzer-Einstellungen

| Methode | Pfad | Auth | Beschreibung |
|---|---|---|---|
| `GET` | `/api/my-key` | Session | API-Key-Prefix anzeigen |
| `POST` | `/api/regen-key` | Session | API-Key neu generieren |
| `GET` | `/api/sharex-config` | Session | ShareX `.sxcu` herunterladen (regeneriert Key) |
| `PATCH` | `/api/user/username` | Session | Benutzername ändern (Passwort erforderlich) |
| `PATCH` | `/api/user/password` | Session | Passwort ändern |
| `PATCH` | `/api/user/email` | Session | E-Mail ändern (sendet Bestätigungs-E-Mail) |
| `PATCH` | `/api/user/language` | Session | UI-Sprache setzen |
| `PATCH` | `/api/user/embed-mode` | Session | Embed-Modus setzen (`embed`/`raw`) |
| `POST` | `/api/user/resend-verification` | Session | Bestätigungs-E-Mail erneut senden |
| `GET` | `/api/user/export` | Session | Daten-Export (GDPR Art. 20) als JSON |
| `DELETE` | `/api/user/account` | Session | Account löschen (GDPR Art. 17, Passwort erforderlich) |
| `GET` | `/api/user/predefined-tags` | Session | Vordefinierte Tags abrufen |
| `PATCH` | `/api/user/predefined-tags` | Session | Vordefinierte Tags aktualisieren |
| `POST` | `/api/user/avatar` | Session | Avatar hochladen (max 2 MB, JPEG/PNG/GIF/WebP) |
| `DELETE` | `/api/user/avatar` | Session | Avatar löschen |
| `GET` | `/api/user/avatar/:userId` | — | Avatar servieren |

Benutzername, Passwort, E-Mail, Bestätigungs-E-Mail und Kontolöschung sind pro Nutzer auf 10 Versuche in 15 Minuten begrenzt (gegen Passwort-Raten mit gestohlener Session und Mail-Spam). Eine Passwort-Änderung beendet alle anderen Sessions des Nutzers; die aktuelle bleibt eingeloggt.

---

### Share-Links

| Methode | Pfad | Auth | Beschreibung |
|---|---|---|---|
| `GET` | `/api/file/:shortId/share-links` | Session (Owner/Admin) | Alle Share-Links einer Datei |
| `POST` | `/api/file/:shortId/share-links` | Session (Owner/Admin) | Share-Link erstellen |
| `DELETE` | `/api/share-links/:token` | Session (Owner/Creator/Admin) | Share-Link löschen |
| `GET` | `/api/share-links/:token` | — | Share-Link-Metadaten (öffentlich; ohne `shortId` der Datei, damit Passwort, Ablauf und Limit nicht über `/f/` umgangen werden können) |
| `POST` | `/api/share-links/:token/verify` | — | Share-Link-Passwort prüfen (max. 10 Fehlversuche pro IP in 15 Minuten) |

**Share-Link erstellen:**
```json
POST /api/file/a1b2c3d4/share-links
{
  "label": "Für Kollegen",
  "password": "geheim",
  "expiresAt": "2025-12-31T23:59:59Z",
  "downloadLimit": 10
}
```

---

### Collections

| Methode | Pfad | Auth | Beschreibung |
|---|---|---|---|
| `GET` | `/api/collections` | Session | Eigene Collections (Admin: alle) |
| `POST` | `/api/collections` | Session | Collection erstellen |
| `GET` | `/api/collections/:id` | — | Collection anzeigen (öffentlich, ggf. Passwort) |
| `PATCH` | `/api/collections/:id` | Session (Owner/Admin) | Collection aktualisieren |
| `DELETE` | `/api/collections/:id` | Session (Owner/Admin) | Collection löschen |
| `POST` | `/api/collections/:id/files` | Session (Owner/Admin) | Datei zur Collection hinzufügen |
| `DELETE` | `/api/collections/:id/files/:fileShortId` | Session (Owner/Admin) | Datei aus Collection entfernen |
| `POST` | `/api/collections/:id/verify` | — | Collection-Passwort prüfen (max. 10 Fehlversuche pro IP in 15 Minuten) |

---

### Admin-Endpunkte

| Methode | Pfad | Auth | Beschreibung |
|---|---|---|---|
| `GET` | `/api/admin/stats` | Admin | Dashboard-Statistiken |
| `GET` | `/api/admin/users` | Admin | Alle Nutzer (inkl. Datei-Counts) |
| `POST` | `/api/admin/users` | Admin | Nutzer erstellen |
| `PATCH` | `/api/admin/users/:id/toggle` | Admin | Nutzer aktivieren/deaktivieren |
| `PATCH` | `/api/admin/users/:id/role` | Admin | Nutzer-Rolle ändern (wirkt sofort, auch auf offene Sessions) |
| `DELETE` | `/api/admin/users/:id` | Admin | Nutzer und alle seine Daten löschen |
| `POST` | `/api/admin/users/:id/regen-key` | Admin | API-Key regenerieren |
| `PATCH` | `/api/admin/users/:id/password` | Admin | Passwort setzen (beendet alle Sessions des Nutzers) |
| `PATCH` | `/api/admin/users/:id/folder` | Admin | Ordnername ändern (verschiebt Dateien) |
| `GET` | `/api/admin/files` | Admin | Alle Dateien, paginiert (30/Seite) |
| `GET` | `/api/admin/site-settings` | Admin | Betreiber-Einstellungen lesen |
| `PATCH` | `/api/admin/site-settings` | Admin | Betreiber-Einstellungen aktualisieren |
| `GET` | `/api/admin/audit-log` | Admin | Paginierter Audit-Log (50/Seite) |
| `GET` | `/api/admin/audit-log/export` | Admin | Audit-Log als CSV herunterladen |
| `GET` | `/api/site-settings` | — | Öffentliche Betreiber-Infos (für Datenschutzseite) |

---

### Site-Settings (öffentlich)

```json
GET /api/site-settings
{
  "operatorName": "Musterfirma GmbH",
  "operatorAddress": "Musterstr. 1, 12345 Musterstadt",
  "operatorEmail": "datenschutz@example.com",
  "cloudflareAnalytics": false,
  "fileRetentionDays": 365,
  "encryptionAtRest": false,
  "sessionDurationDays": 7
}
```

---

## 8. WebSocket API

**Verbindung:** `wss://example.com/ws` (nur für eingeloggte Nutzer, Session-Cookie erforderlich)

### Absicherung

- **Origin-Prüfung:** Der Handshake wird mit `403` abgelehnt, wenn der `Origin`-Header nicht zum `Host` passt (gleiche Regel wie `requireSameOrigin` für die HTTP-API). Der Proxy muss den originalen `Host` weiterreichen.
- **Laufende Rechteprüfung:** Session, Aktiv-Status, Rolle und `sessionVersion` werden bei jeder Nachricht und zusätzlich alle 30 Sekunden für alle Verbindungen neu geprüft. Nach Logout, Deaktivierung, Löschung, Passwort-Änderung oder Herabstufung wird die Verbindung geschlossen bzw. verliert Admin-Rechte und Admin-Broadcasts.
- **Heartbeat:** Ping alle 30 Sekunden; Verbindungen ohne Pong werden getrennt.
- **Limits:** maximal 64 KiB pro Nachricht (größere schließen die Verbindung mit Code `1009`), bis zu 30 Nachrichten am Stück und danach 5 pro Sekunde pro Verbindung (sonst Antwort mit `status: 429`), maximal 20 gleichzeitige Verbindungen pro Client-IP. Aktionen, die das Passwort prüfen oder E-Mails senden, sind pro Nutzer auf 10 in 15 Minuten begrenzt.

### Protokoll

**Client → Server (Request):**
```json
{ "id": "req-abc123", "action": "file:list", "payload": { "type": "image", "page": 1 } }
```

**Server → Client (Response):**
```json
{ "id": "req-abc123", "data": { ... } }
```

**Server → Client (Error):**
```json
{ "id": "req-abc123", "error": "Forbidden", "status": 403 }
```

**Server → Client (Broadcast):**
```json
{ "event": "file:uploaded", "data": { "shortId": "a1b2c3d4", "uploaderId": "..." } }
```

### Verfügbare Actions

| Action | Auth | Beschreibung |
|---|---|---|
| `site-settings:get` | — | Öffentliche Site-Einstellungen |
| `auth:me` | User | Eigene Nutzer-Daten |
| `file:get` | User | Datei-Details (inkrementiert Views) |
| `file:list` | User | Dateiliste mit Filter/Paginierung |
| `file:delete` | User | Datei löschen |
| `user:get-key` | User | API-Key-Prefix |
| `user:regen-key` | User | API-Key neu generieren |
| `user:change-password` | User | Passwort ändern |
| `user:change-username` | User | Benutzername ändern |
| `user:change-email` | User | E-Mail ändern |
| `user:change-language` | User | Sprache setzen |
| `user:change-embed-mode` | User | Embed-Modus setzen |
| `user:resend-verification` | User | Bestätigungs-E-Mail erneut senden |
| `user:export` | User | Daten-Export |
| `user:delete-account` | User | Account löschen |
| `admin:stats` | Admin | Dashboard-Statistiken |
| `admin:settings:get` | Admin | Site-Einstellungen lesen |
| `admin:settings:update` | Admin | Site-Einstellungen aktualisieren |
| `admin:users:list` | Admin | Alle Nutzer |
| `admin:users:create` | Admin | Nutzer erstellen |
| `admin:users:toggle` | Admin | Nutzer aktivieren/deaktivieren |
| `admin:users:role` | Admin | Nutzer-Rolle ändern |
| `admin:users:delete` | Admin | Nutzer löschen |
| `admin:users:regen-key` | Admin | API-Key regenerieren |
| `admin:users:password` | Admin | Passwort setzen |
| `admin:users:folder` | Admin | Ordnername ändern |
| `admin:files:list` | Admin | Alle Dateien |
| `admin:audit-log:list` | Admin | Paginierter Audit-Log |

### Broadcast-Events

| Event | Empfänger | Payload |
|---|---|---|
| `file:uploaded` | Uploader | `{ shortId, uploaderId }` |
| `file:deleted` | Datei-Owner | `{ shortId, uploaderId }` |
| `file:view` | Datei-Owner + Admins | `{ shortId, views }` |
| `user:created` | Admins | `{ id, username, role, ... }` |
| `user:deleted` | Admins | `{ id }` |
| `user:updated` | Admins | `{ id, ...geänderte Felder }` |
| `audit:log` | Admins | Vollständiges AuditLog-Objekt |
| `settings:updated` | Admins | Aktualisiertes SiteSettings-Objekt |
| `stats:invalidate` | Admins | `{}` (Stats-Refresh auslösen) |

---

## 9. Datei-Serving-Routen

### `/f/:shortId` — Datei-Viewer

- **Browser-Aufruf:** Leitet zur React-SPA (`index.html`) durch — SPA rendert den Viewer.
- **Social-Media-Bot** (Discord, Telegram, Twitter, etc.): Liefert eine minimale HTML-Seite mit Open Graph / Twitter Card Meta-Tags.
  - `embedMode = 'embed'`: OG-HTML mit Redirect
  - `embedMode = 'raw'` + Bild/Video/Audio: HTTP 302 → `/f/:shortId/raw`

### `/f/:shortId/raw` — Direktzugriff

Liefert die Datei als HTTP-Response mit Range-Request-Unterstützung (206 Partial Content, auch Suffix-Ranges wie `bytes=-500`). Verschlüsselte Dateien werden dabei live entschlüsselt.

Welche Dateien direkt im Browser angezeigt werden, entscheidet eine Whitelist (`src/utils/mime.js`) auf Basis des normalisierten MIME-Typs:

| Fall | `Content-Type` | `Content-Disposition` | Zusätzlich |
|---|---|---|---|
| Bilder (PNG, JPEG, GIF, WebP, AVIF, BMP, ICO), gängige Video-/Audioformate, PDF | echter Typ | `inline` | — |
| Text und Code (nach `displayType`) | `text/plain; charset=utf-8` | `inline` | `CSP: sandbox` |
| HTML, SVG, XML und alles andere | `application/octet-stream` | `attachment` | `CSP: sandbox` |

So kann eine hochgeladene Datei weder als Seite im App-Origin gerendert noch als Skript eingebunden werden.

Hat die Datei ein Download-Limit (`maxDownloads`), zählt `/raw` nur vollständige Abrufe (ohne `Range` oder ab Byte 0), und zwar einmal pro Besucher-Session: Vorschau und anschließender Download desselben Besuchers verbrauchen zusammen einen Download. Abrufe von Eigentümer und Admins zählen nie. Ist das Limit erreicht, antwortet die Route mit `410`.

### `/f/:shortId/download` — Force-Download

Wie `/raw`, aber immer `Content-Disposition: attachment` und `application/octet-stream`. Jeder neue Besucher zählt gegen `maxDownloads`.

### `/f/:shortId/thumb` — Thumbnail

Liefert JPEG-Thumbnail (für Videos und PDFs), bei aktivierter Verschlüsselung entschlüsselt. `Cache-Control: public, max-age=86400`. Die ID muss aus `[A-Za-z0-9_-]` bestehen (Schutz gegen Path Traversal über `%2F`).

### `/f/:shortId/delete/:token` — ShareX-Deletion

Löscht Datei ohne Session via eindeutigem Delete-Token. Der Token wird nur beim Upload zurückgegeben und taucht in keiner anderen API-Antwort auf.

### `/s/:token` — Share-Link-Datei-Serving (`src/routes/shares.js`)

Prüft Passwort (via Session-Flag), Ablaufdatum und Download-Limit, dann liefert die Datei aus.

### Schutz vor Durchprobieren von IDs

Alle öffentlichen Abrufe über ID oder Token (`/f/*`, `/s/*`, `/api/file/:shortId`, `/api/share-links/:token`, `/api/collections/:id`, Pastes) teilen sich ein Rate-Limit: maximal 100 fehlgeschlagene Abrufe (404/410/401 …) pro IP in 15 Minuten. Erfolgreiche Abrufe zählen nicht. Ältere Datei-IDs mit 8 Zeichen bleiben gültig, sind aber kürzer und damit leichter zu erraten als neue.

---

## 10. Authentifizierung & Sicherheit

### Session-Authentifizierung

- Express-Session mit MongoDB-Store (`connect-mongo`)
- Session-Cookie: `httpOnly: true`, `sameSite: 'strict'`, `secure: 'auto'` (gesetzt, sobald der Request per HTTPS ankommt; hinter einem Proxy über `X-Forwarded-Proto` und `TRUST_PROXY`)
- Session-Dauer: konfigurierbar über `SiteSettings.sessionDurationDays` (Standard: 7 Tage), Live-gecacht für 60 Sekunden
- Login, Registrierung und Installation vergeben eine neue Session-ID (`establishSession`, Schutz gegen Session-Fixation)
- Bei jedem Request werden Rolle, Aktiv-Status und `sessionVersion` des Nutzers aus der Datenbank gelesen: Gelöschte oder deaktivierte Konten und Sessions von vor einer Passwort-Änderung verlieren sofort ihre Anmeldung, Rollenänderungen gelten sofort

### API-Key-Authentifizierung

- API-Keys werden als SHA-256-Hash gespeichert, nie im Klartext
- Die ersten 8 Zeichen werden als `apiKeyPrefix` gespeichert (für Anzeigezwecke)
- Lookup: `User.findByApiKey(plaintext)` berechnet Hash und sucht in `apiKeyHash`
- Beim Download der ShareX-Konfiguration wird der Key neu generiert — der Plaintext ist nur in diesem Moment sichtbar

### Middleware-Kette (`src/middleware/auth.js`)

```
requireLogin    → prüft req.session.user → 401 wenn nicht eingeloggt
requireAdmin    → wie requireLogin, zusätzlich role === 'admin' → 403
requireApiKey   → prüft Authorization: Bearer, X-API-Key, ?api_key= oder req.body.token
```

Alle Router werden über `asyncRouter()` erzeugt: Fehler in `async`-Handlern landen beim zentralen Error-Handler (`500`), statt den Request hängen zu lassen. Eine `uncaughtException` beendet den Prozess, Docker startet ihn neu (`restart: unless-stopped`).

### CSRF-Schutz

`requireSameOrigin()` in `app.js` vergleicht `Origin`-Header mit `Host`-Header für alle API-Routen. Ergänzt `sameSite: 'strict'`-Cookies.

### Content Security Policy

```
default-src 'self';
script-src 'self' https://static.cloudflareinsights.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
media-src 'self' blob:;
connect-src 'self' https://cloudflareinsights.com;
frame-ancestors 'self';
base-uri 'none';
form-action 'self';
object-src 'none';
```

### Security-Headers

- `Strict-Transport-Security: max-age=31536000; includeSubDomains` (nur bei HTTPS-Requests)
- `X-Frame-Options: SAMEORIGIN`
- `X-Content-Type-Options: nosniff`
- `Referrer-Policy: same-origin` (Datei-, Paste- und Share-URLs gelangen nicht per Referer an fremde Seiten)
- `Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()`

### Rate-Limiting

| Endpunkt | Limit | Fenster | Schlüssel |
|---|---|---|---|
| Upload (`/upload`, `/api/upload`, `/api/web-upload`) | 60 Requests | 15 Minuten | IP |
| Auth (`/api/auth/login`, `/register`, etc.) | 10 Requests | 15 Minuten | IP |
| Passwort-Reset | 5 Requests | 1 Stunde | IP |
| Passwort-Prüfung von Share-Links, Collections, Pastes | 10 Fehlversuche | 15 Minuten | IP |
| Konto-Aktionen (Name, Passwort, E-Mail, Bestätigungsmail, Löschung) | 10 Requests | 15 Minuten | Nutzer |
| Fehlgeschlagene ID-/Token-Abrufe (siehe Kapitel 9) | 100 Requests | 15 Minuten | IP |
| WebSocket | siehe Kapitel 8 | — | Verbindung / IP / Nutzer |

Die Client-IP hängt von `TRUST_PROXY` ab; der App-Port muss deshalb nur über den Reverse Proxy erreichbar sein (`BIND_ADDRESS`), sonst lässt sich `X-Forwarded-For` fälschen.

### Logging

Das Access-Log (stdout → Docker-Log) enthält gekürzte IP-Adressen (IPv4 auf `/24`, IPv6 auf `/48`). API-Keys (`api_key=`), Tokens in Query-Parametern (`token=`), Delete-Tokens und Share-Tokens werden geschwärzt. Das Audit-Log speichert die vollständige IP und wird nach 90 Tagen automatisch gelöscht.

### Datei-Blockliste

Folgende MIME-Types und Erweiterungen werden auf Upload abgelehnt:

**Blockierte MIME-Types:** `application/x-executable`, `application/x-sh`, `application/x-csh`, `application/x-bat`

**Blockierte Erweiterungen:** `.bat`, `.cmd`, `.com`, `.ps1`, `.psm1`, `.psd1`, `.sh`, `.bash`, `.csh`, `.zsh`, `.fish`, `.vbs`, `.vbe`, `.jse`, `.scr`, `.pif`, `.application`, `.gadget`, `.hta`, `.php`, `.php3–5`, `.phtml`, `.asp`, `.aspx`, `.jsp`, `.jspx`, `.cfm`

### Path-Traversal-Schutz

Alle Datei-Zugriffe über `resolveUploadPath()` prüfen, ob der aufgelöste Pfad innerhalb von `UPLOAD_DIR` liegt. Thumbnail-IDs werden zusätzlich gegen `[A-Za-z0-9_-]` geprüft.

---

## 11. Upload-System

### Standard-Upload (Multer)

- **Storage:** `multer.diskStorage` → `uploads/{folderName}/{8hex}{.ext}`
- **Dateiname:** `crypto.randomBytes(4).hex() + original-extension`
- **Limit:** `MAX_FILE_SIZE_MB` (Standard: 100 MB)
- **Speicherort:** Nutzer-spezifischer Ordner (`user.folderName`)
- **MIME-Typ:** wird mit `normalizeMime()` auf genau einen `typ/subtyp` reduziert (Parameter und kommagetrennte Zusatztypen fallen weg, ungültige Werte werden `application/octet-stream`)
- **Verschlüsselung:** mit `ENCRYPTION_KEY` verschlüsselt `sealUpload()` die Datei an Ort und Stelle; eine Klartext-Kopie (`<datei>.plain-<hex>`) bleibt nur bis zur Thumbnail-Erzeugung bestehen
- **ShareX-Endpunkt `/upload`:** Multer schreibt die Datei vor der API-Key-Prüfung; wird der Request abgelehnt, wird sie sofort wieder gelöscht

### Chunked Upload (>250 MB)

Der Frontend-Client wechselt automatisch in den Chunked-Modus für große Dateien.

**Server-seitige Verzeichnisstruktur:**
```
uploads/.chunks/{uploadId}/
  meta.json          { filename, mimeType, totalSize, totalChunks, userId, createdAt }
  chunk-0
  chunk-1
  ...
  chunk-N
```

**Chunk-Größe:** 10–20 MB (max 51 MB akzeptiert für Rückwärtskompatibilität)  
**Parallelität:** 3–5 gleichzeitige Chunk-Uploads  
**Assemblierung:** Stream-basiert (kein vollständiges Laden in RAM)  
**Größenprüfung:** empfangene Bytes dürfen `totalSize` nicht überschreiten; die fertige Datei muss exakt `totalSize` groß sein  
**Aufräumen:** nicht abgeschlossene Upload-Verzeichnisse werden nach 24 Stunden vom Retention-Job gelöscht

### Avatar-Upload

- **Storage:** `multer.memoryStorage()` (kein Festplatten-Zwischenspeicher)
- **Speicherort:** `uploads/.avatars/{userId}{.ext}`
- **Limit:** 2 MB
- **Formate:** JPEG, PNG, GIF, WebP

---

## 12. Thumbnail-Generierung

Thumbnails werden asynchron nach dem Upload generiert (`startThumbnail()` — Fehler werden ignoriert). Mit Docker Compose laufen ffmpeg und Ghostscript nicht im App-Container, sondern im eigenen Container `thumbnailer`: ohne Netzwerk, ohne Secrets, als Nutzer `nobody`, mit schreibgeschütztem Dateisystem und ohne Zugriff auf `uploads/`. Die App kopiert jede Quelldatei in das gemeinsame Volume `thumbjobs` (`in/`), der Worker (`src/thumbnailWorker.js`) legt das Ergebnis in `out/` ab, und die App übernimmt es nur als reguläre Datei bis 2 MB (keine Symlinks). Läuft der Container nicht, werden Thumbnails übersprungen und die App loggt einmalig eine Warnung. Ohne `THUMBNAIL_WORKER_DIR` (lokale Entwicklung) laufen die Tools direkt in der App, mit leerer Umgebung (nur `PATH`).

> **Upgrade von älteren Versionen:** einmalig `docker compose up -d --build` ausführen, damit der Container `thumbnailer` angelegt wird. Mit Updater diesen ebenfalls einmal neu bauen (`docker compose --profile updater up -d --build updater`), ältere Updater erneuern nur den App-Container.

Ist `ENCRYPTION_KEY` gesetzt, wird das Thumbnail aus der Klartext-Kopie erzeugt, danach verschlüsselt und die Kopie gelöscht. Scheitert die Erzeugung, wird ein eventuell halb geschriebenes Thumbnail entfernt.

### Video-Thumbnails (ffmpeg)

```bash
ffmpeg -y -i <file> -ss 00:00:01 -vframes 1 \
  -vf "scale=320:320:force_original_aspect_ratio=increase,crop=320:320" \
  -q:v 3 uploads/.thumbnails/<shortId>.jpg
```

### PDF-Thumbnails (Ghostscript)

```bash
gs -dNOPAUSE -dBATCH -dSAFER \
  -sDEVICE=jpeg -dFirstPage=1 -dLastPage=1 \
  -r72 -dJPEGQ=85 \
  -sOutputFile=uploads/.thumbnails/<shortId>.jpg \
  <file>
```

**Timeout:** 30 Sekunden pro Thumbnail-Generierung  
**Fallback:** Wenn ffmpeg/ghostscript nicht verfügbar, wird die Generierung lautlos übersprungen.

### Backfill-Skript

```bash
npm run migrate:thumbnails
# oder im Container:
docker exec -it <container> npm run migrate:thumbnails
```

Für verschlüsselte Dateien entschlüsselt das Skript die Quelle kurzzeitig in eine temporäre Datei und verschlüsselt das erzeugte Thumbnail.

---

## 13. E-Mail & SMTP

### Konfiguration

SMTP wird aktiviert, wenn `SMTP_HOST` gesetzt ist. `mailer.isConfigured()` prüft diesen Wert.

### E-Mail-Templates

Alle E-Mails werden in der Sprache des Nutzers gesendet (8 Sprachen). Templates sind in `src/utils/mailer.js` eingebettet.

**Gesendete E-Mail-Typen:**

| Typ | Trigger | Token-Gültigkeit |
|---|---|---|
| E-Mail-Bestätigung | Registrierung, E-Mail-Änderung | 24 Stunden |
| Passwort-Reset | `POST /api/auth/forgot-password` | 1 Stunde |

### Token-Sicherheit

- Tokens: `crypto.randomBytes(32).hex()` (64 Hex-Zeichen)
- Gespeichert: SHA-256-Hash des Tokens
- E-Mail enthält Klartext-Token als URL-Parameter
- Verifizierung: Hash des eingehenden Tokens gegen gespeicherten Hash

---

## 14. Internationalisierung

**Bibliothek:** i18next + react-i18next + i18next-browser-languagedetector

**Unterstützte Sprachen:**

| Code | Sprache |
|---|---|
| `en` | English |
| `de` | Deutsch |
| `fr` | Français |
| `es` | Español |
| `it` | Italiano |
| `pt` | Português |
| `ja` | 日本語 |
| `zh` | 中文 |

**Sprachauswahl:**
1. Browser-Language-Detection (automatisch)
2. Nutzer-Einstellung in der Datenbank (`user.language`)
3. Persistierung über `PATCH /api/user/language`

Übersetzungsdateien: `client/src/i18n/locales/{code}.json`

---

## 15. Frontend (React SPA)

### Router-Konfiguration (`client/src/App.jsx`)

| Route | Komponente | Auth |
|---|---|---|
| `/` | Redirect → `/gallery` | Nein |
| `/auth/login` | Login.jsx | Nein |
| `/auth/register` | Register.jsx | Nein |
| `/auth/forgot-password` | ForgotPassword.jsx | Nein |
| `/auth/reset-password` | ResetPassword.jsx | Nein |
| `/install` | Install.jsx | Nein |
| `/upload` | Upload.jsx | **Ja** |
| `/gallery` | Gallery.jsx | **Ja** |
| `/f/:shortId` | FileView.jsx | **Ja** |
| `/collections` | Collections.jsx | **Ja** |
| `/c/:id` | CollectionView.jsx | Nein (öffentlich) |
| `/s/:token` | ShareView.jsx | Nein (öffentlich) |
| `/settings` | Settings.jsx | **Ja** |
| `/admin` | Dashboard.jsx | **Admin** |
| `/admin/users` | Users.jsx | **Admin** |
| `/admin/files` | Files.jsx | **Admin** |
| `/admin/audit-log` | AuditLog.jsx | **Admin** |
| `/admin/site-settings` | SiteSettings.jsx | **Admin** |
| `/admin/import` | Import.jsx | **Admin** |
| `/privacy` | PrivacyPolicy.jsx | Nein |
| `/terms` | TermsOfService.jsx | Nein |

### Auth-Context (`client/src/context/AuthContext.jsx`)

Globaler State für den eingeloggten Nutzer. Wird beim App-Start via `GET /api/auth/me` initialisiert.

### WebSocket-Hook (`client/src/hooks/useWebSocket.js`)

Verwaltet die persistente WS-Verbindung. Stellt `sendMessage()` und Event-Handler-Registration bereit. Reconnect-Logik bei Verbindungsabbruch.

### UI-Komponenten

Basiert auf **shadcn/ui** (Radix UI Primitives + Tailwind CSS):

- `Dialog`, `AlertDialog`, `DropdownMenu`, `ContextMenu`, `Popover`
- `Select`, `Checkbox`, `Input`, `Textarea`, `Label`
- `Card`, `Badge`, `Button`, `Separator`, `Tabs`, `Table`
- `Toast` / `Toaster` für Benachrichtigungen
- `Calendar` / `DateTimePicker` für Ablaufdatum-Auswahl
- `Pagination` für paginierte Listen
- `ScrollArea`, `Tooltip`

---

## 16. Admin-Dashboard

Das Admin-Dashboard (`/admin/*`) ist nur für Nutzer mit `role: 'admin'` zugänglich.

### Dashboard (`/admin`)

- Gesamtanzahl Nutzer, Dateien, Speicherverbrauch
- 10 zuletzt hochgeladene Dateien
- Live-Updates via WebSocket (`stats:invalidate`)

### Nutzerverwaltung (`/admin/users`)

- Alle Nutzer mit Datei-Count und Speichernutzung
- Nutzer erstellen, aktivieren/deaktivieren, Rolle ändern (Deaktivierung und Herabstufung wirken sofort, auch auf offene Sessions und WebSocket-Verbindungen)
- Passwort zurücksetzen (meldet den Nutzer überall ab), API-Key regenerieren
- Ordnername ändern (physische Dateien werden auf dem Server verschoben)
- Nutzer löschen (alle Daten wie bei der Selbstlöschung, siehe Kapitel 17)

### Dateiverwaltung (`/admin/files`)

- Alle Dateien aller Nutzer, paginiert
- Suche nach Dateiname
- Datei löschen

### Audit-Log (`/admin/audit-log`)

- Paginierter Log aller Aktionen (50/Seite)
- Filter nach Nutzername und Aktion
- CSV-Export für regulatorische Zwecke

### Site-Einstellungen (`/admin/site-settings`)

- Betreiber-Informationen (Name, Adresse, E-Mail)
- Cloudflare Analytics aktivieren
- Datei-Retention-Zeitraum
- Session-Dauer
- Registrierung aktivieren/deaktivieren

### XBackBone-Import (`/admin/import`)

- Vorschau und Import aus XBackBone SQLite-Datenbank
- Nutzer werden nach Username gematcht
- Idempotent (bereits importierte Dateien werden übersprungen)

---

## 17. GDPR / Datenschutz

| Feature | DSGVO-Artikel |
|---|---|
| Datenschutzerklärung (konfigurierbar) | Art. 13/14 – Transparenz |
| AGB-Seite (konfigurierbar) | Art. 13/14 – Transparenz |
| Daten-Export (JSON mit URLs) | Art. 20 – Datenportabilität |
| Account-Löschung (Dateien, Pastes, Collections, Share-Links, Avatar) | Art. 17 – Löschrecht |
| Audit-Log (90-Tage-TTL via MongoDB) | Art. 5(2) – Rechenschaftspflicht |
| Audit-Log-CSV-Export | Art. 5(2) – Rechenschaftspflicht |
| Konfigurierbarer Datei-Retention | Art. 5(1)(e) – Speicherbegrenzung |
| API-Keys als SHA-256-Hash | Art. 32 – Sicherheit |
| Passwörter als bcrypt (12 Rounds) | Art. 32 – Sicherheit |
| Cookie-Consent für Cloudflare Analytics | Art. 13 – Transparenz |
| Anonymisierung bei Account-Löschung | Art. 17 – Löschrecht |
| Optionale Verschlüsselung gespeicherter Dateien (`ENCRYPTION_KEY`) | Art. 32 – Sicherheit |
| Gekürzte IP-Adressen und geschwärzte Tokens im Access-Log | Art. 5(1)(c) – Datenminimierung |

### GDPR-Lösch-Flow

Bei Account-Löschung (`user:delete-account` / `DELETE /api/user/account`) und beim Löschen durch einen Admin (`admin:users:delete` / `DELETE /api/admin/users/:id`) läuft derselbe Ablauf (`src/utils/deleteUserData.js`):
1. Alle Dateien des Nutzers werden von der Festplatte gelöscht
2. Thumbnails werden gelöscht
3. Share-Links des Nutzers und auf seine Dateien werden gelöscht
4. Pastes und Collections des Nutzers werden gelöscht; seine Dateien werden auch aus Collections anderer Nutzer entfernt
5. Avatar wird gelöscht
6. Audit-Log-Einträge werden anonymisiert (`username: '[deleted]'`, `ip: null`, `userId: null`)
7. User-Dokument wird gelöscht
8. Bei Selbstlöschung: Session wird zerstört; offene Sessions und WebSocket-Verbindungen verlieren beim nächsten Request ihre Anmeldung

Der Audit-Eintrag der Löschung selbst bleibt als Nachweis bestehen und wird nach 90 Tagen automatisch gelöscht.

---

## 18. Hintergrund-Jobs

### Retention-Cleanup (`src/jobs/retentionCleanup.js`)

- **Ausführung:** Beim Start und dann täglich (`setInterval(runRetentionCleanup, 24h)`)
- **Aktion:** Wenn `SiteSettings.fileRetentionDays > 0`, werden Dateien gelöscht, die älter als dieser Wert sind; außerdem selbstzerstörende Dateien nach `expiresAt` oder erreichtem `maxDownloads`
- Löscht: Datei auf Festplatte, Thumbnail, MongoDB-Dokument
- Räumt auf: nicht abgeschlossene Chunk-Uploads älter als 24 Stunden sowie liegengebliebene Klartext-Kopien verschlüsselter Uploads (`*.plain-*`) älter als 1 Stunde
- Broadcastet `stats:invalidate` an Admins

### MongoDB-TTL-Indizes (automatisch)

| Collection | TTL | Trigger |
|---|---|---|
| `AuditLog` | 90 Tage | `timestamp` |
| `Collection` | 7 Tage nach `expiresAt` | `expiresAt` |
| `ShareLink` | 7 Tage nach `expiresAt` | `expiresAt` |

---

## 19. Deployment

### Docker (empfohlen)

```bash
git clone https://github.com/Christianoooooo/sharely.git
cd sharely
cp .env.example .env
# .env bearbeiten (SESSION_SECRET, MONGO-Passwörter, BASE_URL)
docker compose up -d
```

**Services:**
- `app` — Node.js-App (Port 3000, standardmäßig nur auf `127.0.0.1` veröffentlicht, siehe `BIND_ADDRESS`)
- `mongo` — MongoDB 9 (Port 127.0.0.1:27017, nicht von außen erreichbar)
- `updater` — optionaler Self-Updater (nur mit Profil `updater`, siehe „Automatische Updates“)

**Volumes:**
- `uploads` — Persistente Datei-Ablage
- `mongo_data` — MongoDB-Daten
- `control` — Austausch zwischen App und Updater

**Health-Checks:** App prüft HTTP 200 auf `/`, MongoDB prüft `db.adminCommand('ping')`.

### Nginx-Reverse-Proxy

```nginx
server {
    listen 443 ssl;
    server_name files.example.com;

    client_max_body_size 2100M;  # Mind. so groß wie größter Chunk + Puffer

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket-Support
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
```

> `BASE_URL` in `.env` muss der öffentlichen Domain entsprechen.

### Cloudflare Tunnel / Proxy im Container

Docker veröffentlicht den App-Port standardmäßig nur auf `127.0.0.1` (`BIND_ADDRESS`). Ein Proxy, der direkt auf dem Host läuft, wie im nginx-Beispiel oben, erreicht ihn. Ein Proxy oder `cloudflared` in einem eigenen Container erreicht ihn nicht und antwortet mit **502 Bad Gateway**.

- **Empfohlen:** Den Proxy-Container in das Docker-Netz von sharely hängen (`<projekt>_default`, standardmäßig `sharely_default`) und auf `http://app:3000` zeigen lassen. Dann muss gar kein Port veröffentlicht werden:
  ```bash
  docker network connect sharely_default <cloudflared-or-proxy-container>
  ```
  Damit das Neustarts übersteht, das Netz in der Compose-Datei des Proxys als `external` eintragen.
- **Mit externer Firewall**, die Port 3000 aus dem Internet blockiert (z. B. Hetzner Cloud Firewall): `BIND_ADDRESS=0.0.0.0` in der `.env` setzen. Ohne eine solche Firewall nicht machen: Docker umgeht Host-Firewalls wie `ufw`, und wer den Port direkt erreicht, umgeht TLS und kann `X-Forwarded-For` fälschen, womit die Rate-Limits wirkungslos sind.

Der Proxy muss den originalen `Host`-Header weiterreichen (WebSocket-Verbindungen werden abgelehnt, wenn `Origin` und `Host` nicht übereinstimmen) sowie `X-Forwarded-Proto` (nötig für Secure-Cookies und HSTS). Cloudflare Tunnel und das nginx-Beispiel tun das standardmäßig; in der Tunnel-Konfiguration kein `httpHostHeader` setzen. Nach der Einrichtung prüfen, ob im Audit-Log echte Client-IPs stehen; sonst `TRUST_PROXY` anpassen.

> **Upgrade-Hinweis:** Frühere Versionen haben Port 3000 auf allen Interfaces veröffentlicht. Läuft der Proxy in einem Container, vor dem Update `BIND_ADDRESS` setzen oder das Netz wie oben beschrieben verbinden.

### Verschlüsselung gespeicherter Dateien

Ist `ENCRYPTION_KEY` gesetzt, verschlüsselt sharely jeden neuen Upload und sein Thumbnail vor dem Speichern mit AES-256-GCM (ein eigener, aus dem Hauptschlüssel abgeleiteter Schlüssel pro Datei). Beim Ausliefern wird live entschlüsselt, auch für Range-Requests und ZIP-Downloads.

- Schlüssel einmalig mit `openssl rand -base64 32` erzeugen, in die `.env` eintragen und **getrennt sichern**: Ohne ihn lassen sich verschlüsselte Dateien nicht wiederherstellen. Ein späterer Wechsel oder das Entfernen des Schlüssels wird nicht unterstützt.
- Bestehende Dateien bleiben unverschlüsselt, bis die Migration läuft (vorher das `uploads`-Volume sichern): `docker compose exec app npm run migrate:encrypt`
- Nicht abgedeckt: Pastes, Dateinamen und andere Metadaten in MongoDB, Avatare sowie der Arbeitsspeicher des laufenden Servers. Während ein Upload verarbeitet wird (Zusammensetzen der Chunks, Metadaten-Entfernung, Thumbnail-Erzeugung), liegt er kurz unverschlüsselt auf der Platte.
- Die Admin-Einstellung zur Festplatten-/Volume-Verschlüsselung erklärt nur eine Volume-Verschlüsselung in der Datenschutzerklärung und ist davon unabhängig.

### Erster Start

Beim ersten Start ohne Nutzer schreibt sharely ein Setup-Token ins Log (`docker compose logs app`), alternativ gilt `INSTALL_TOKEN`, falls gesetzt. Die Installationsseite verlangt dieses Token und legt damit das Admin-Konto an; über die normale Registrierung entsteht nie ein Admin.

---

## 20. Entwicklungsumgebung

### Voraussetzungen

- Node.js ≥ 22
- MongoDB (lokal oder via Docker)
- Optional: ffmpeg, ghostscript (für Thumbnails)

### Setup

```bash
# Backend
npm install
cp .env.example .env
# .env anpassen

# Frontend
cd client
npm install
```

### Starten

```bash
# Backend (Port 3000, mit nodemon)
npm run dev

# Frontend (Port 5173, separates Terminal)
cd client
npm run dev
```

Der Vite-Dev-Server proxyt API-Requests automatisch an `localhost:3000`.

### Build

```bash
npm run build   # Baut client/dist/, Backend bleibt unverändert
npm start       # Startet produktiven Express-Server
```

---

## 21. Migrationen & Skripte

### Automatische Migrationen (bei jedem Start)

Diese Migrationen laufen beim App-Start in `app.js` und sind idempotent:

| Migration | Datei | Funktion |
|---|---|---|
| User-Folder-Migration | `src/migrations/migrateUserFolders.js` | Verschiebt Dateien aus `uploads/` in `uploads/{folderName}/` |
| API-Key-Hash-Migration | `src/migrations/migrateApiKeyHashes.js` | Konvertiert Klartext-API-Keys zu SHA-256-Hashes |

### Manuelle Skripte

```bash
# Thumbnails für bereits vorhandene Dateien generieren
npm run migrate:thumbnails
# oder:
node scripts/generate-missing-thumbnails.js

# Uploads in Nutzer-Ordner verschieben (manuell)
npm run migrate:user-folders
# oder:
node scripts/migrate-uploads-to-user-folders.js

# Bestehende Dateien und Thumbnails verschlüsseln (ENCRYPTION_KEY nötig, vorher Backup)
npm run migrate:encrypt
# oder im Container:
docker compose exec app npm run migrate:encrypt
```

`migrate:encrypt` schreibt jede Datei verschlüsselt nach `<storedName>.enc`, stellt danach den Datensatz um und löscht erst dann den Klartext. Ein abgebrochener Lauf lässt sich gefahrlos wiederholen.

### Datenbankinitialisierung (Docker)

`scripts/mongo-init.js` wird beim ersten Start des MongoDB-Containers ausgeführt und erstellt den App-Nutzer mit den richtigen Berechtigungen.

---

## 22. End-to-End-Tests

**Framework:** Playwright (`@playwright/test`)

### Test-Dateien

| Datei | Testsuite |
|---|---|
| `e2e/upload.spec.js` | Upload-Flows |
| `e2e/gallery.spec.js` | Galerie und Datei-Verwaltung |
| `e2e/admin.spec.js` | Admin-Dashboard |
| `e2e/sharelink.spec.js` | Share-Link-Erstellung und -Nutzung |
| `e2e/tags.spec.js` | Tag-Verwaltung |
| `e2e/bulk-actions-fixes.spec.js` | Bulk-Aktionen |

### Ausführen

```bash
# Alle Tests
npm run test:e2e

# Mit UI
npm run test:e2e:ui
```

**Playwright-Konfiguration:** `playwright.config.js` (startet den Server mit `INSTALL_TOKEN`, Standard `e2e-install-token`)  
**Global Setup:** `e2e/global-setup.js` (führt die Installation mit diesem Token aus, erstellt Test-Nutzer, -Admin etc.)  
**Helfer:** `e2e/helpers.js` (gemeinsame Hilfsfunktionen)

### Unit-Tests

```bash
npm test
```

Läuft ohne Datenbank: `src/utils/validateEmail.test.js`, `src/utils/stripMetadata.test.js` und `src/utils/fileCrypto.test.js` (Verschlüsselung: Roundtrip, Byte-Bereiche, Erkennung von Manipulation).

---

## Anhang: Audit-Log-Aktionen

| Aktion | Trigger |
|---|---|
| `login` | Erfolgreicher Login |
| `logout` | Logout |
| `register` | Registrierung |
| `upload` | Datei-Upload |
| `delete_file` | Datei gelöscht |
| `delete_account` | Account gelöscht |
| `change_password` | Passwort geändert |
| `change_username` | Benutzername geändert |
| `change_email` | E-Mail geändert |
| `verify_email` | E-Mail bestätigt |
| `forgot_password` | Passwort-Reset angefordert |
| `reset_password` | Passwort zurückgesetzt |
| `regen_api_key` | API-Key regeneriert |
| `sharex_config` | ShareX-Konfiguration heruntergeladen |
| `export_data` | Daten-Export |
| `admin_create_user` | Admin: Nutzer erstellt |
| `admin_delete_user` | Admin: Nutzer gelöscht |
| `admin_toggle_user` | Admin: Nutzer aktiviert/deaktiviert |
| `admin_change_role` | Admin: Nutzer-Rolle geändert |
| `admin_change_password` | Admin: Passwort gesetzt |
| `admin_regen_key` | Admin: API-Key regeneriert |

---

*Dokumentation generiert aus dem Quellcode von sharely v1.0.0*