feat(web): HTTP-Basic-Auth fuer alle Routen; webUser/webPassword in NVS; ElegantOTA.setAuth()
This commit is contained in:
parent
18a1f67f64
commit
07c99dc7d8
|
|
@ -157,9 +157,9 @@
|
|||
- [x] **6.3** Publish-Methode `publishSession(sessionSec, summeSession, gratisSec)`:
|
||||
- Topic: `lasercutter/session`
|
||||
- JSON-Felder (aktualisiert):
|
||||
- `session_minuten` (int, ceiling: 62 s = 2 min)
|
||||
- `session_minutes` (int, ceiling: 62 s = 2 min)
|
||||
- `session_seconds` (int, Rohwert Netto-Sekunden)
|
||||
- `gratiszeit_s` (int)
|
||||
- `freetime_s` (int)
|
||||
- `ip` (string)
|
||||
- Kein `total_min`, kein `session_start_time` (NTP später)
|
||||
- Wird von LaserTracker-Logik in main aufgerufen
|
||||
|
|
@ -262,6 +262,15 @@
|
|||
- Reset während aktiver Session ✅
|
||||
- Neustart nach akkumulierter Zeit → Zeit korrekt aus NVS geladen ✅
|
||||
|
||||
- [x] **9.6** Webinterface mit HTTP-Basic-Auth abgesichert
|
||||
- `webUser` + `webPassword` in `Settings`-Struct + NVS-Keys in `config.h`
|
||||
- `saveWebCredentials()` in `SettingsManager`
|
||||
- `WebServerManager::requireAuth()` – prüft Auth, sendet 401 wenn nicht autorisiert
|
||||
- Standard: `webUser = "admin"`, `webPassword = ""` (leer = kein Schutz, rückwärtskompatibel)
|
||||
- Alle Routen (`/`, `/config`, `/reset`) + ElegantOTA (`ElegantOTA.setAuth()`) geschützt
|
||||
- Passwort änderbar über `/config`-Formular (neuer Abschnitt "Web-Zugang")
|
||||
- Forward-Declaration `AsyncWebServerRequest` in `web_server.h` ergänzt (Fix Compile-Fehler)
|
||||
|
||||
---
|
||||
|
||||
## Phase 10 – Dokumentation & Abschluss
|
||||
|
|
@ -306,9 +315,9 @@ Phase 1 (Setup)
|
|||
**`lasercutter/session`**
|
||||
```json
|
||||
{
|
||||
"session_minuten": 2,
|
||||
"session_minutes": 2,
|
||||
"session_seconds": 125,
|
||||
"gratiszeit_s": 20,
|
||||
"freetime_s": 20,
|
||||
"ip": "192.168.1.100"
|
||||
}
|
||||
```
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -218,10 +218,10 @@ Der ESP32 verbindet sich mit einem MQTT-Broker (konfigurierbar über Webinterfac
|
|||
### JSON-Format `lasercutter/session`
|
||||
```json
|
||||
{
|
||||
"session_minuten": 2,
|
||||
"session_minutes": 2,
|
||||
"session_seconds": 95,
|
||||
"session_start_time": "2026-02-23T12:34:56Z",
|
||||
"gratiszeit_s": 20,
|
||||
"freetime_s": 20,
|
||||
"ip": "192.168.2.62"
|
||||
}
|
||||
```
|
||||
|
|
@ -261,10 +261,12 @@ Das Webinterface ist über die IP-Adresse des ESP32 im Browser erreichbar.
|
|||
| Seite | URL | Funktion |
|
||||
|------------------|-------------|--------------------------------------------------------------|
|
||||
| Status | `/` | Aktuelle Laserzeit, letzter Session-Wert, Systemstatus |
|
||||
| Konfiguration | `/config` | MQTT-Broker (IP, Port, User, Passwort), Gratiszeit, Polarität |
|
||||
| Konfiguration | `/config` | MQTT-Broker (IP, Port, User, Passwort), Gratiszeit, Polarität, Web-Passwort |
|
||||
| Reset | `/reset` | Setzt akkumulierte Laserzeit zurück |
|
||||
| OTA Update | `/update` | Firmware-Update über Browser |
|
||||
|
||||
> Alle Seiten sind per **HTTP Basic Auth** geschützt, wenn in der Konfiguration ein Web-Passwort gesetzt ist.
|
||||
|
||||
---
|
||||
|
||||
## Konfiguration & Persistenz
|
||||
|
|
@ -280,6 +282,8 @@ Alle Einstellungen werden im **NVS (Non-Volatile Storage)** des ESP32 gespeicher
|
|||
| Gratiszeit | `20` | Sekunden, 0–120 |
|
||||
| Signal-Polarität | `LOW_ACTIVE` | `LOW_ACTIVE` oder `HIGH_ACTIVE` |
|
||||
| Akkumulierte Zeit | `0.0` | Gespeicherte Laserzeit in Minuten (NVS) |
|
||||
| Web-Benutzername | `admin` | HTTP-Basic-Auth Benutzername |
|
||||
| Web-Passwort | *(leer)* | Leer = kein Passwortschutz; setzen über `/config` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -84,11 +84,17 @@
|
|||
#define NVS_KEY_GRATIS_SEC "gratis_sec"
|
||||
#define NVS_KEY_SIGNAL_POL "signal_pol" // 0 = LOW_ACTIVE, 1 = HIGH_ACTIVE
|
||||
#define NVS_KEY_TOTAL_MINUTES "total_min"
|
||||
#define NVS_KEY_WEB_USER "web_user"
|
||||
#define NVS_KEY_WEB_PASSWORD "web_pass"
|
||||
|
||||
// Signal-Polarität Werte
|
||||
#define SIGNAL_POL_LOW_ACTIVE 0 // LOW = Laser aktiv (INPUT_PULLUP Default)
|
||||
#define SIGNAL_POL_HIGH_ACTIVE 1 // HIGH = Laser aktiv
|
||||
|
||||
// Webinterface-Authentifizierung
|
||||
#define DEFAULT_WEB_USER "admin"
|
||||
#define DEFAULT_WEB_PASSWORD "" // leer = kein Passwortschutz
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Serielles Debugging
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ struct Settings {
|
|||
uint8_t gratisSeconds; // Gratiszeit in Sekunden (0–120)
|
||||
uint8_t signalPolarity; // SIGNAL_POL_LOW_ACTIVE oder _HIGH_ACTIVE
|
||||
float totalMinutes; // akkumulierte Laserzeit in Minuten
|
||||
char webUser[32]; // Webinterface Benutzername (Standard: "admin")
|
||||
char webPassword[32]; // Webinterface Passwort (leer = kein Schutz)
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -54,6 +56,7 @@ public:
|
|||
void saveSignalPolarity(uint8_t polarity);
|
||||
void saveMqttConfig(const char* broker, uint16_t port,
|
||||
const char* user, const char* password);
|
||||
void saveWebCredentials(const char* user, const char* password);
|
||||
|
||||
// ---- Zugriff auf Werte --------------------------------------------------
|
||||
const Settings& get() const { return _s; }
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@
|
|||
|
||||
#include <Arduino.h>
|
||||
|
||||
// Forward-Declaration: ESPAsyncWebServer.h nur in web_server.cpp einbinden
|
||||
// Forward-Declarations: ESPAsyncWebServer.h nur in web_server.cpp einbinden
|
||||
// (verhindert HTTP_GET/POST-Enum-Konflikt mit WiFiManager in main.cpp)
|
||||
class AsyncWebServer;
|
||||
class AsyncWebServerRequest;
|
||||
|
||||
class WebServerManager {
|
||||
public:
|
||||
|
|
@ -37,6 +38,9 @@ private:
|
|||
|
||||
void registerRoutes();
|
||||
|
||||
// HTTP-Basic-Auth pruefen; gibt false zurueck und sendet 401 wenn nicht autorisiert
|
||||
bool requireAuth(AsyncWebServerRequest* request);
|
||||
|
||||
// HTML-Seiten als Strings aufbauen
|
||||
String buildStatusPage();
|
||||
String buildConfigPage();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ void SettingsManager::applyDefaults() {
|
|||
_s.gratisSeconds = DEFAULT_GRATIS_SECONDS;
|
||||
_s.signalPolarity = SIGNAL_POL_LOW_ACTIVE;
|
||||
_s.totalMinutes = 0.0f;
|
||||
strlcpy(_s.webUser, DEFAULT_WEB_USER, sizeof(_s.webUser));
|
||||
strlcpy(_s.webPassword, DEFAULT_WEB_PASSWORD, sizeof(_s.webPassword));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -64,6 +66,12 @@ void SettingsManager::load() {
|
|||
_s.gratisSeconds = _prefs.getUChar(NVS_KEY_GRATIS_SEC, DEFAULT_GRATIS_SECONDS);
|
||||
_s.signalPolarity = _prefs.getUChar(NVS_KEY_SIGNAL_POL, SIGNAL_POL_LOW_ACTIVE);
|
||||
_s.totalMinutes = _prefs.getFloat(NVS_KEY_TOTAL_MINUTES, 0.0f);
|
||||
strlcpy(_s.webUser,
|
||||
_prefs.getString(NVS_KEY_WEB_USER, DEFAULT_WEB_USER).c_str(),
|
||||
sizeof(_s.webUser));
|
||||
strlcpy(_s.webPassword,
|
||||
_prefs.getString(NVS_KEY_WEB_PASSWORD, DEFAULT_WEB_PASSWORD).c_str(),
|
||||
sizeof(_s.webPassword));
|
||||
|
||||
validateAndClamp();
|
||||
}
|
||||
|
|
@ -77,6 +85,8 @@ void SettingsManager::save() {
|
|||
_prefs.putUChar (NVS_KEY_GRATIS_SEC, _s.gratisSeconds);
|
||||
_prefs.putUChar (NVS_KEY_SIGNAL_POL, _s.signalPolarity);
|
||||
_prefs.putFloat (NVS_KEY_TOTAL_MINUTES, _s.totalMinutes);
|
||||
_prefs.putString(NVS_KEY_WEB_USER, _s.webUser);
|
||||
_prefs.putString(NVS_KEY_WEB_PASSWORD, _s.webPassword);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -120,6 +130,14 @@ void SettingsManager::saveMqttConfig(const char* broker, uint16_t port,
|
|||
_prefs.putString(NVS_KEY_MQTT_PASSWORD, password);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
void SettingsManager::saveWebCredentials(const char* user, const char* password) {
|
||||
strlcpy(_s.webUser, user, sizeof(_s.webUser));
|
||||
strlcpy(_s.webPassword, password, sizeof(_s.webPassword));
|
||||
_prefs.putString(NVS_KEY_WEB_USER, user);
|
||||
_prefs.putString(NVS_KEY_WEB_PASSWORD, password);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
void SettingsManager::printToSerial() const {
|
||||
LOG_I("NVS", "=== Geladene Einstellungen ===");
|
||||
|
|
@ -131,5 +149,7 @@ void SettingsManager::printToSerial() const {
|
|||
LOG_I("NVS", " Signalpolarit : %s",
|
||||
_s.signalPolarity == SIGNAL_POL_LOW_ACTIVE ? "LOW_ACTIVE" : "HIGH_ACTIVE");
|
||||
LOG_I("NVS", " Laserzeit ges.: %.2f min", _s.totalMinutes);
|
||||
LOG_I("NVS", " Web User : %s", _s.webUser);
|
||||
LOG_I("NVS", " Web Passwort : %s", _s.webPassword[0] ? "***" : "(kein Schutz)");
|
||||
LOG_I("NVS", "==============================");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,11 +32,30 @@ WebServerManager::WebServerManager() : _server(new AsyncWebServer(80)) {}
|
|||
// -----------------------------------------------------------------------------
|
||||
void WebServerManager::begin() {
|
||||
registerRoutes();
|
||||
// ElegantOTA-Auth: nur wenn Passwort gesetzt
|
||||
const Settings& s = settings.get();
|
||||
if (s.webPassword[0] != '\0') {
|
||||
ElegantOTA.setAuth(s.webUser, s.webPassword);
|
||||
}
|
||||
ElegantOTA.begin(_server);
|
||||
_server->begin();
|
||||
Serial.println(F("[WebServer] gestartet auf Port 80"));
|
||||
Serial.print(F("[WebServer] URL: http://"));
|
||||
Serial.println(WiFi.localIP());
|
||||
LOG_I("WEB", "Auth: %s", s.webPassword[0] ? "aktiv" : "deaktiviert (kein Passwort)");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// requireAuth() – HTTP Basic Auth pruefen (nur wenn Passwort konfiguriert)
|
||||
// -----------------------------------------------------------------------------
|
||||
bool WebServerManager::requireAuth(AsyncWebServerRequest* request) {
|
||||
const Settings& s = settings.get();
|
||||
if (s.webPassword[0] == '\0') return true; // kein Passwort = kein Schutz
|
||||
if (!request->authenticate(s.webUser, s.webPassword)) {
|
||||
request->requestAuthentication("LaserCutter Display");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
@ -46,16 +65,19 @@ void WebServerManager::registerRoutes() {
|
|||
|
||||
// --- GET / – Statusseite -------------------------------------------------
|
||||
_server->on("/", HTTP_GET, [this](AsyncWebServerRequest* request) {
|
||||
if (!requireAuth(request)) return;
|
||||
request->send(200, "text/html; charset=utf-8", buildStatusPage());
|
||||
});
|
||||
|
||||
// --- GET /config – Konfigurationsformular --------------------------------
|
||||
_server->on("/config", HTTP_GET, [this](AsyncWebServerRequest* request) {
|
||||
if (!requireAuth(request)) return;
|
||||
request->send(200, "text/html; charset=utf-8", buildConfigPage());
|
||||
});
|
||||
|
||||
// --- POST /config – Konfiguration speichern ------------------------------
|
||||
_server->on("/config", HTTP_POST, [](AsyncWebServerRequest* request) {
|
||||
_server->on("/config", HTTP_POST, [this](AsyncWebServerRequest* request) {
|
||||
if (!requireAuth(request)) return;
|
||||
const Settings& s = settings.get();
|
||||
|
||||
// MQTT
|
||||
|
|
@ -87,6 +109,13 @@ void WebServerManager::registerRoutes() {
|
|||
settings.saveSignalPolarity(pol);
|
||||
}
|
||||
|
||||
// Webinterface-Passwort
|
||||
if (request->hasParam("web_user", true) && request->hasParam("web_password", true)) {
|
||||
const char* wUser = request->getParam("web_user", true)->value().c_str();
|
||||
const char* wPass = request->getParam("web_password", true)->value().c_str();
|
||||
settings.saveWebCredentials(wUser, wPass);
|
||||
}
|
||||
|
||||
Serial.println(F("[WebServer] Konfiguration gespeichert – Neustart empfohlen"));
|
||||
|
||||
// Weiterleitung mit Hinweis
|
||||
|
|
@ -102,7 +131,8 @@ void WebServerManager::registerRoutes() {
|
|||
});
|
||||
|
||||
// --- POST /reset – Session zurücksetzen ---------------------------------
|
||||
_server->on("/reset", HTTP_POST, [](AsyncWebServerRequest* request) {
|
||||
_server->on("/reset", HTTP_POST, [this](AsyncWebServerRequest* request) {
|
||||
if (!requireAuth(request)) return;
|
||||
laserTracker.resetSession();
|
||||
Serial.println(F("[WebServer] Session zurueckgesetzt"));
|
||||
request->send(200, "text/html; charset=utf-8",
|
||||
|
|
@ -157,10 +187,11 @@ String WebServerManager::buildStatusPage() {
|
|||
"<body>"
|
||||
"<h1>LaserCutter Display</h1>"
|
||||
"<table>"
|
||||
"<tr><td>Summe Session</td><td>%ALLSESSIONS% min</td></tr>"
|
||||
"<tr><td>Maschinenlaufzeit (gesamt)</td><td>%TOTAL% min</td></tr>"
|
||||
"<tr><td>Summe Laserzeit</td><td>%ALLSESSIONS% min</td></tr>"
|
||||
"<tr><td>Aktuelle Laserzeit</td><td>%CURRENTSESSION% s</td></tr>"
|
||||
"<tr><td>Letzte Laserzeit</td><td>%LASTSESSION% s</td></tr>"
|
||||
"<tr><td>Laser</td><td>%LASER%</td></tr>"
|
||||
"<tr><td>Maschinenlaufzeit (gesamt)</td><td>%TOTAL% min</td></tr>"
|
||||
"<tr><td>IP-Adresse</td><td>%IP%</td></tr>"
|
||||
"<tr><td>MQTT</td><td>%MQTT%</td></tr>"
|
||||
"<tr><td>Broker</td><td>%BROKER%:%PORT%</td></tr>"
|
||||
|
|
@ -179,6 +210,7 @@ String WebServerManager::buildStatusPage() {
|
|||
);
|
||||
|
||||
html.replace("%ALLSESSIONS%", String(laserTracker.getAllSessionsSumMinutes()));
|
||||
html.replace("%CURRENTSESSION%", String(laserTracker.getRunningSessionSeconds()));
|
||||
html.replace("%TOTAL%", String(totalBuf));
|
||||
html.replace("%LASTSESSION%", String(laserTracker.getLastSessionSeconds()));
|
||||
html.replace("%LASER%", laserStatus);
|
||||
|
|
@ -234,6 +266,13 @@ String WebServerManager::buildConfigPage() {
|
|||
"<label class='radio-label'>"
|
||||
"<input type='radio' name='polarity' value='1' %POL_HIGH%>"
|
||||
"HIGH-aktiv</label>"
|
||||
"<hr style='margin-top:1.5rem'>"
|
||||
"<h2 style='font-size:1.1rem'>Web-Zugang</h2>"
|
||||
"<label>Benutzername</label>"
|
||||
"<input type='text' name='web_user' value='%WEB_USER%' maxlength='31'>"
|
||||
"<label>Passwort <small style='color:#888'>(leer = kein Schutz)</small></label>"
|
||||
"<input type='password' name='web_password' value='' maxlength='31' "
|
||||
"placeholder='unverändert lassen wenn leer'>"
|
||||
"<br>"
|
||||
"<button class='btn btn-primary' type='submit'>Speichern & Zurück</button>"
|
||||
"</form><br>"
|
||||
|
|
@ -248,6 +287,7 @@ String WebServerManager::buildConfigPage() {
|
|||
html.replace("%GRATIS%", String(s.gratisSeconds));
|
||||
html.replace("%POL_LOW%", s.signalPolarity == 0 ? "checked" : "");
|
||||
html.replace("%POL_HIGH%", s.signalPolarity == 1 ? "checked" : "");
|
||||
html.replace("%WEB_USER%", String(s.webUser));
|
||||
|
||||
return html;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ pio device monitor -e test-mqtt
|
|||
3. Serial: `SessionEnd -> publishSession`
|
||||
4. MQTT Explorer: `lasercutter/session` erscheint mit JSON:
|
||||
```json
|
||||
{"session_minuten": 1, "session_seconds": 5, "gratiszeit_s": 20, "ip": "192.168.x.x"}
|
||||
{"session_minutes": 1, "session_seconds": 5, "freetime_s": 20, "ip": "192.168.x.x"}
|
||||
```
|
||||
|
||||
**C) Gratis-Session (< 20 s):**
|
||||
|
|
@ -342,7 +342,7 @@ pio device monitor -e test-mqtt
|
|||
...
|
||||
[LaserTracker] SessionEnd: gesamt=35s netto=15s sessionNetSec=15 total=0.58min
|
||||
[I][TEST-MQTT] SessionEnd -> publishSession (lastSession=15s)
|
||||
[MQTT] publishSession: {"session_minuten":1,"session_seconds":15,"gratiszeit_s":20,"ip":"192.168.x.x"} -> OK
|
||||
[MQTT] publishSession: {"session_minutes":1,"session_seconds":15,"freetime_s":20,"ip":"192.168.x.x"} -> OK
|
||||
```
|
||||
|
||||
### Checkliste
|
||||
|
|
@ -352,7 +352,7 @@ pio device monitor -e test-mqtt
|
|||
| Verbindung zu mqtt.majufilo.eu | `[MQTT] Verbunden!` im Serial | [x] |
|
||||
| Heartbeat beim Connect | `lasercutter/status` retained im MQTT Explorer | [x] |
|
||||
| Heartbeat alle 60 s | Topic aktualisiert sich periodisch | [x] |
|
||||
| Session-Publish nach Session-Ende | `lasercutter/session` mit `session_minuten`, `session_seconds` | [x] |
|
||||
| Session-Publish nach Session-Ende | `lasercutter/session` mit `session_minutes`, `session_seconds` | [x] |
|
||||
| Gratis-Session publiziert NICHT | kein Topic-Update bei lastSession=0 | [ ] |
|
||||
| Reset via MQTT (`lasercutter/reset` = `{"reset":true}`) | `total_min` springt auf 0 | [x] |
|
||||
| LWT `{"online":false}` nach Verbindungsabbruch | Nach 60 s im MQTT Explorer sichtbar | [x] |
|
||||
|
|
@ -409,6 +409,14 @@ IP-Adresse aus dem Serial Monitor ablesen (erscheint nach WiFi-Connect):
|
|||
2. ElegantOTA-Seite erscheint
|
||||
3. (Optional) Test-Firmware hochladen und Neustart beobachten
|
||||
|
||||
**E) HTTP-Basic-Auth:**
|
||||
1. In `/config` Abschnitt "Web-Zugang": Passwort setzen (z.B. `geheim`) und speichern
|
||||
2. ESP32 neu starten
|
||||
3. Browser: `http://<IP>/` → Browser-Login-Dialog erscheint
|
||||
4. Benutzername `admin`, Passwort `geheim` eingeben → Zugang gewährt
|
||||
5. Falsches Passwort eingeben → Browser zeigt 401 / erneuten Dialog
|
||||
6. In `/config` Passwort wieder leeren → nach Neustart kein Schutz mehr
|
||||
|
||||
### Checkliste
|
||||
|
||||
| Pruefpunkt | Erwartung | Ergebnis |
|
||||
|
|
@ -421,6 +429,10 @@ IP-Adresse aus dem Serial Monitor ablesen (erscheint nach WiFi-Connect):
|
|||
| Polaritaet aendern + speichern | NVS-Wert nach Neustart korrekt | [ ] |
|
||||
| Session-Reset per Browser | Session = 0 min, Gesamtzeit (NVS) unveraendert | [ ] |
|
||||
| `/update` erreichbar | ElegantOTA-Seite erscheint | [ ] |
|
||||
| Passwort setzen + Neustart | Browser-Login-Dialog erscheint | [ ] |
|
||||
| Korrektes Passwort | Zugang gewaehrt | [ ] |
|
||||
| Falsches Passwort | 401 / erneuter Dialog | [ ] |
|
||||
| Passwort leeren + Neustart | Kein Login-Dialog | [ ] |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user