# barox SN Check

Webtool zur Prüfung von Seriennummern und Garantiestatus von barox Switches.
Ein Benutzer meldet sich mit seinem **Microsoft 365 Konto** an, gibt eine
Seriennummer ein, und das Tool ermittelt über die **SelectLine API** die
Lieferinformationen aus dem ERP und berechnet daraus den Garantiestatus.

* **Produktion:** https://sncheck.barox.io
* **Installationspfad:** `/var/www/html/barox-sncheck`
* **Stack:** Ubuntu · Apache · PHP 8.1+ (FPM) · MySQL · Composer

---

## Inhalt

1. [Funktionsübersicht](#funktionsübersicht)
2. [Projektstruktur](#projektstruktur)
3. [Voraussetzungen](#voraussetzungen)
4. [Installation](#installation)
5. [Konfiguration (`.env`)](#konfiguration-env)
6. [Microsoft 365 / Azure AD einrichten](#microsoft-365--azure-ad-einrichten)
7. [SelectLine API – **zu verifizieren**](#selectline-api--zu-verifizieren)
8. [Datenbank / Migrationen](#datenbank--migrationen)
9. [Apache VirtualHost](#apache-virtualhost)
10. [Dateirechte für `storage/`](#dateirechte-für-storage)
11. [Wartung / Cronjobs](#wartung--cronjobs)
12. [Sicherheit](#sicherheit)

---

## Funktionsübersicht

* **Login** ausschließlich über Microsoft 365 (OAuth2 / OpenID Connect,
  Authorization Code Flow mit PKCE). Zugriff nur für freigeschaltete Benutzer
  (Domain- **oder** Benutzer-Whitelist). Sichere Sessions, Logout.
* **Seriennummer-Suche**: Eingabe wird getrimmt, normalisiert und validiert,
  dann über die SelectLine API gesucht. Mehrere Treffer werden aufgelistet,
  kein Treffer wird klar gemeldet.
* **Anzeige** als Statuskarte: Seriennummer, Artikelnummer, Artikelbezeichnung,
  Kunde/Debitor, Lieferscheinnummer, Lieferdatum, Garantieende, Garantiestatus
  (grün *In Garantie* / rot *Garantie abgelaufen* / grau *Nicht bestimmbar*)
  inkl. verbleibender Tage.
* **Garantie-Logik**: `Lieferdatum + Garantiezeit = Garantieende`. Garantiezeit
  konfigurierbar (`.env` **oder** DB-Tabelle `settings`, Standard 5 Jahre).
* **Datenbank**: Login-Historie, Suchhistorie, optionaler API-Cache,
  Laufzeit-Konfiguration, Rate-Limit-Zähler.
* **Sicherheit**: Prepared Statements, CSRF, XSS-Escaping, CSP, sichere
  Session-Cookies, Rate-Limiting, serverseitige Validierung, neutrale
  Fehlerseiten, Details nur im Log.

---

## Projektstruktur

```
/var/www/html/barox-sncheck
├── public/                 # DocumentRoot (einziger web-erreichbarer Ordner)
│   ├── index.php           # Front Controller
│   ├── .htaccess           # Routing + Header
│   └── assets/             # css, js, img, fonts (barox CD)
├── src/
│   ├── Application.php      # Bootstrap / Container / Routing
│   ├── helpers.php          # e() (Escaping), asset()
│   ├── Auth/                # MicrosoftAuthProvider, SessionManager, AuthException
│   ├── SelectLine/          # SelectLineClient, WarrantyCalculator, SelectLineException
│   ├── Database/            # Database (PDO) + Repositories
│   ├── Controller/          # Auth, Home, Search
│   ├── Http/                # Request, Response, Router
│   ├── Support/             # Config, Logger, Csrf, RateLimiter, View, Validator
│   ├── Exception/           # HttpException
│   └── View/                # layout, login, home, error (PHP-Templates)
├── config/
│   └── app.php              # zentrale, getypte Konfiguration (liest .env)
├── database/migrations/     # SQL-Migrationen (001…005)
├── bin/
│   ├── migrate.php          # Migrations-Runner
│   └── purge.php            # Datenschutz-Aufräumjob
├── deploy/apache/           # Beispiel-VirtualHost
├── storage/
│   ├── logs/                # app.log, api.log (rotierend)
│   ├── cache/               # Laufzeit-Cache (optional)
│   └── sessions/            # Session-Dateien
├── .env.example
├── composer.json
└── README.md
```

---

## Voraussetzungen

* PHP **8.1+** mit den Erweiterungen: `pdo_mysql`, `curl`, `openssl`, `json`, `mbstring`
* Composer 2
* MySQL 5.7+ / MariaDB 10.4+
* Apache 2.4 mit `mod_rewrite`, `mod_ssl`, `mod_headers`, `mod_expires`
* PHP-FPM (empfohlen)

```bash
sudo apt update
sudo apt install apache2 php-fpm php-mysql php-curl php-mbstring php-xml composer mysql-server -y
```

---

## Installation

```bash
# 1. Code nach /var/www/html/barox-sncheck bringen (Git-Clone oder Deploy)
cd /var/www/html/barox-sncheck

# 2. Abhängigkeiten installieren (Produktions-Set, ohne Dev-Pakete)
composer install --no-dev --optimize-autoloader

# 3. Konfiguration anlegen
cp .env.example .env
#    -> .env mit realen Werten füllen (siehe unten)
php -r "echo 'APP_KEY='.bin2hex(random_bytes(32)).PHP_EOL;"   # Wert in .env eintragen

# 4. Datenbank + Migrationen
mysql -u root -p -e "CREATE DATABASE barox_sncheck CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
php bin/migrate.php

# 5. Rechte für storage/ setzen (siehe Abschnitt weiter unten)
sudo chown -R www-data:www-data storage
sudo find storage -type d -exec chmod 770 {} \;

# 6. Apache VirtualHost einrichten (siehe Abschnitt weiter unten) und neu laden
```

### Composer-Abhängigkeiten

| Paket                | Zweck                                          |
|----------------------|------------------------------------------------|
| `vlucas/phpdotenv`   | `.env` laden                                   |
| `guzzlehttp/guzzle`  | HTTP-Client (Microsoft + SelectLine)           |
| `monolog/monolog`    | Logging (`storage/logs`)                       |

---

## Konfiguration (`.env`)

Alle Werte stehen in `.env.example` mit Kommentaren. Die wichtigsten:

| Schlüssel | Bedeutung |
|-----------|-----------|
| `APP_URL` | öffentliche URL, z. B. `https://sncheck.barox.io` |
| `APP_KEY` | zufälliger 32-Byte-Schlüssel (`openssl rand -hex 32`) |
| `DB_*` | MySQL Host, Port, DB, User, Passwort |
| `MS_TENANT_ID` / `MS_CLIENT_ID` / `MS_CLIENT_SECRET` | Azure-App-Registrierung |
| `MS_REDIRECT_URI` | `https://sncheck.barox.io/auth/callback` |
| `ALLOWED_DOMAINS` | erlaubte E-Mail-Domains (z. B. `barox.io,barox.ch`) |
| `ALLOWED_USERS` | optionale, explizite Benutzer-Whitelist (überschreibt Domainprüfung) |
| `SELECTLINE_BASE_URL` | Basis-URL der SelectLine API (Standard für beide Mandanten) |
| `SELECTLINE_AG_*` | Zugang **Mandant barox AG** – eigener `_USER`/`_PASSWORD`/`_APPKEY` (optional `_BASE_URL`/`_LABEL`) |
| `SELECTLINE_GMBH_*` | Zugang **Mandant barox GmbH** (analog, eigener API-Benutzer/AppKey) |
| `WARRANTY_YEARS` | Garantiezeit in Jahren (Standard 5) |
| `API_TIMEOUT` | Timeout für API-Aufrufe in Sekunden |

> **Wichtig:** `.env` enthält Secrets und darf **niemals** ins Git-Repository
> gelangen (bereits in `.gitignore`). Nur `.env.example` wird versioniert.

Die **Garantiezeit** kann alternativ zur `.env` in der DB-Tabelle `settings`
(`warranty_years`) hinterlegt werden – dieser Wert hat Vorrang und lässt sich
ohne Deploy ändern.

---

## Microsoft 365 / Azure AD einrichten

1. **Azure Portal → App-Registrierungen → Neue Registrierung**
   * Name: `barox SN Check`
   * Unterstützte Kontotypen: *Nur Konten in diesem Organisationsverzeichnis*
   * Redirect-URI (Typ **Web**): `https://sncheck.barox.io/auth/callback`
2. **Übersicht**: *Anwendungs-ID (Client)* → `MS_CLIENT_ID`,
   *Verzeichnis-ID (Mandant)* → `MS_TENANT_ID`.
3. **Zertifikate & Geheimnisse → Neuer geheimer Clientschlüssel** →
   Wert → `MS_CLIENT_SECRET`.
4. **API-Berechtigungen**: `openid`, `profile`, `email`, `User.Read`
   (delegiert, Microsoft Graph). Admin-Zustimmung erteilen.
5. Optional in der App **Zuweisung erforderlich** aktivieren und nur bestimmte
   Benutzer/Gruppen zuweisen. Zusätzlich greift die `ALLOWED_DOMAINS` /
   `ALLOWED_USERS`-Prüfung in der Anwendung.

---

## SelectLine API – **zu verifizieren**

> ⚠️ Die exakte SelectLine-API-Struktur (Login-Antwort, Such-Endpunkt,
> Feldnamen) hängt von der konkreten Installation/Version ab und **muss vor
> Produktivbetrieb verifiziert werden.** Alle veränderlichen Teile sind an
> **einer** zentralen Stelle gebündelt:
> [`config/app.php`](config/app.php) → Abschnitt `selectline`.

### Zwei Mandanten (barox AG & barox GmbH)

Es gibt **zwei SelectLine-Mandanten**. Jeder wird in der `.env` mit eigenen
Zugangsdaten hinterlegt (`SELECTLINE_AG_*` / `SELECTLINE_GMBH_*`). Der
[`SelectLineService`](src/SelectLine/SelectLineService.php) durchsucht **beide**
Mandanten und markiert jeden Treffer mit seinem Mandanten (Anzeige als Chip in
der Ergebniskarte + Feld „Mandant"). Fällt ein Mandant aus, liefert der andere
weiterhin Ergebnisse; im Frontend erscheint dann ein Hinweis auf ein
möglicherweise unvollständiges Ergebnis.

**Gewählter Ansatz:** **Eigener API-Benutzer + AppKey je Mandant.** Es genügt
also, pro Mandant `SELECTLINE_AG_USER`/`_PASSWORD`/`_APPKEY` bzw.
`SELECTLINE_GMBH_*` zu setzen – der Mandant ergibt sich aus den Zugangsdaten.
Ein zusätzliches Mandantenfeld im Login wird **nicht** benötigt
(`SELECTLINE_*_MANDANT` bleibt leer).

> Alternativ unterstützt der Client auch ein Mandantenfeld im Login-Payload
> (`SELECTLINE_MANDANT_FIELD` + `SELECTLINE_*_MANDANT`) oder getrennte
> API-Server (`SELECTLINE_*_BASE_URL`) – hier nicht erforderlich.

Anzupassen sind dort:

| Konfigurationsschlüssel | Bedeutung / zu prüfen |
|-------------------------|-----------------------|
| `endpoints.login` | Login-Route. Der Request-Body (`UserName`/`Password`/`AppKey`) und der Token-Feldname in der Antwort werden in [`SelectLineClient::login()`](src/SelectLine/SelectLineClient.php) gelesen (unterstützt `AccessToken`/`Token`/`SessionId`/`LoginId`). |
| `endpoints.serial_search` | Such-Route für Seriennummern. `{sn}` wird durch die URL-kodierte Seriennummer ersetzt. |
| `serial_search_method` | `GET` oder `POST`. |
| `serial_search_body` | Body-Template bei `POST` (`{sn}`-Platzhalter). |
| `results_path` | Pfad zur Trefferliste in der Antwort (z. B. `value` bei OData). Leer = Antwort ist bereits die Liste. |
| `field_map` | Mapping **SelectLine-Feldname → Anzeigefeld** (`serialNumber`, `articleNumber`, `articleDescription`, `customer`, `customerNumber`, `deliveryNoteNumber`, `deliveryDate`). Punkt-Notation für verschachtelte Felder erlaubt. |
| `date_formats` | akzeptierte Datumsformate des Lieferdatums. |

**Konkret zu klären mit dem SelectLine-Verantwortlichen:**

1. Wie lautet die Login-Route und welches Feld enthält das Session-/Access-Token?
2. Wie wird das Token bei Folgeanfragen übergeben (Header `Authorization: Bearer …`
   oder `LoginId: …`)? Der Client setzt aktuell **beides** – bei Bedarf in
   [`SelectLineClient::send()`](src/SelectLine/SelectLineClient.php) reduzieren.
3. Über welche Route/Struktur ist eine **Seriennummer** samt zugehörigem
   **Lieferschein** (Nummer + Datum) und **Kunde** abfragbar? In SelectLine
   liegt die Seriennummer typischerweise an einer Belegposition eines
   Lieferscheins – ggf. ist ein Makro/gespeicherte Abfrage nötig, das genau
   diese Felder in einem Datensatz zurückliefert.
4. Wie heißen die realen Felder für Lieferdatum, Lieferscheinnummer, Kunde,
   Artikelnummer/-bezeichnung? → in `field_map` eintragen.

Bis zur Klärung sind sinnvolle Platzhalter hinterlegt; die Fehlerbehandlung
(nicht erreichbar / Auth fehlgeschlagen / Timeout / ungültige Antwort / keine
Treffer) ist vollständig implementiert und wird nach `storage/logs/api.log`
protokolliert.

---

## Datenbank / Migrationen

```bash
php bin/migrate.php     # idempotent, merkt sich angewandte Dateien in "migrations"
```

Tabellen: `login_history`, `search_history`, `api_cache`, `rate_limits`,
`settings` (+ `migrations`). Details siehe [`database/migrations/`](database/migrations/).

---

## Apache VirtualHost

Beispiel: [`deploy/apache/sncheck.barox.io.conf`](deploy/apache/sncheck.barox.io.conf).

```bash
sudo cp deploy/apache/sncheck.barox.io.conf /etc/apache2/sites-available/
sudo a2enmod rewrite ssl headers expires proxy_fcgi
sudo a2ensite sncheck.barox.io
sudo systemctl reload apache2
```

* **DocumentRoot** zeigt auf `/var/www/html/barox-sncheck/public`.
* `src/`, `config/`, `storage/`, `database/`, `vendor/`, `bin/` liegen außerhalb
  des Webroots **und** werden per `DirectoryMatch` zusätzlich gesperrt.
* Passe `SSLCertificateFile`/`-KeyFile` und die PHP-FPM-Socket-Version an.

---

## Dateirechte für `storage/`

`storage/` muss für den Webserver-Benutzer (meist `www-data`) beschreibbar sein,
der restliche Code sollte es **nicht** sein.

```bash
# Eigentümer der Laufzeitordner = Webserver
sudo chown -R www-data:www-data /var/www/html/barox-sncheck/storage

# Verzeichnisse 770, Dateien 660
sudo find /var/www/html/barox-sncheck/storage -type d -exec chmod 770 {} \;
sudo find /var/www/html/barox-sncheck/storage -type f -exec chmod 660 {} \;

# Code nur lesbar für den Webserver (Beispiel)
sudo chown -R deploy:www-data /var/www/html/barox-sncheck/{src,public,config,vendor}
```

Benötigt werden: `storage/logs`, `storage/cache`, `storage/sessions`.

---

## Wartung / Cronjobs

Datenschutz-Aufräumjob (löscht alte Suchbegriffe gemäß
`SEARCH_HISTORY_RETENTION_DAYS` sowie abgelaufene Cache-/Rate-Limit-Einträge):

```cron
15 3 * * * www-data php /var/www/html/barox-sncheck/bin/purge.php >> /var/www/html/barox-sncheck/storage/logs/purge.log 2>&1
```

Logs liegen rotierend (14 Tage) unter `storage/logs/` – `app.log` (Anwendung)
und `api.log` (SelectLine). **Keine** sensiblen API-Zugangsdaten werden geloggt.

---

## Sicherheit

* **Transport:** HTTPS erzwungen (HSTS im vHost), sichere Cookies
  (`Secure`, `HttpOnly`, `SameSite=Lax`).
* **AuthN:** OIDC Authorization Code Flow **mit PKCE**, `state`/`nonce`.
  Das ID-Token wird direkt vom Token-Endpoint über eine TLS-gesicherte
  Server-zu-Server-Verbindung bezogen; die Claims (`aud`/`iss`/`nonce`/`exp`/`nbf`)
  werden vollständig geprüft (OpenID Connect Core 3.1.3.7).
* **AuthZ:** Domain-/Benutzer-Whitelist; ohne Konfiguration wird **niemand**
  zugelassen (sicherer Standard).
* **Injection:** ausschließlich Prepared Statements (kein String-Concat mit
  Benutzereingaben).
* **XSS:** serverseitig `e()` in allen Templates, clientseitig DOM-Aufbau via
  `textContent`, strikte **Content-Security-Policy**.
* **CSRF:** Token pro Session, zeitkonstanter Vergleich; Such-Endpoint prüft es.
* **Rate-Limiting:** pro Benutzer (`RATE_LIMIT_MAX` / `RATE_LIMIT_WINDOW`).
* **Fehler:** Benutzer sehen neutrale Meldungen; technische Details nur im Log.

---

© barox Kommunikation – internes Werkzeug.
