Skip to content

Commit f8298d9

Browse files
fhirschmannclaude
andcommitted
feat: authenticate the API via HTTP Basic Auth + document fork endpoints
Web_IsAuthenticated now also accepts the web password sent per-request via HTTP Basic Auth (fixed username "espuino"), so API clients, scripts and the Swagger UI can authenticate without the cookie/login flow (e.g. 'curl -u espuino:<pw>'). The existing session-cookie path is unchanged, and with no web password set the API stays open. Also fills the OpenAPI spec (REST-API.yaml, served at /swagger.html): adds all fork-added endpoints (/sync, /syncstop, /playlist, /githubupdate, /version, /login, /logout, /security, /eqrule(s), /activeequalizer, /bluetooth*, /sdclean) and declares a basicAuth security scheme so 'Authorize' works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 694f710 commit f8298d9

3 files changed

Lines changed: 298 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
115115
| SD card cleanup: removes macOS metadata (`.DS_Store`, `._*`, Spotlight/Trashes) with one click | [`4e68541`](../../commit/4e68541) |
116116
| Live log: the log dialog refreshes every 2 s and follows the end of the log | [`5183e78`](../../commit/5183e78) |
117117
| Drag & drop: upload files by dropping them from the file manager onto the file tree | [`f8b477b`](../../commit/f8b477b) |
118-
| Password protection: single password (no username), 90-day session cookie, logout menu entry, brute-force lockout; inactive in hotspot mode | [`e74e712`](../../commit/e74e712) |
118+
| Password protection: single password (no username), 90-day session cookie, logout menu entry, brute-force lockout; inactive in hotspot mode. API clients/scripts can also authenticate per-request via **HTTP Basic Auth** (username `espuino`, password = the web password, e.g. `curl -u espuino:<pw>`) instead of the cookie/login flow | [`e74e712`](../../commit/e74e712) |
119119
| Log download as a text file | [`5d9d591`](../../commit/5d9d591) |
120120
| Battery indicator in the navbar with a low-battery warning toast | [`4566ae0`](../../commit/4566ae0) |
121121
| SD card capacity gauge in the files tab and the info dialog | [`45b340a`](../../commit/45b340a) |
@@ -135,6 +135,7 @@ neon logo that doubles as the SVG favicon ([`7be5254`](../../commit/7be5254)):
135135
| **HTTP file sync**: pull audio files from a web server (e.g. nginx) onto the SD card. A new **Sync** settings sub-tab takes a manifest URL plus optional HTTP Basic Auth (user/password); the device fetches a JSON manifest (`{"files":[{"path","size"},…]}`) and downloads every listed file that is missing locally or differs in size, streaming each in small chunks straight to SD. Additive only (never deletes), runs in a background task, and shows live progress including the file currently being downloaded plus a stop button to cancel a running sync. The **Sync** settings sub-tab holds the config (manifest URL + Basic Auth) and documents the manifest format/nginx setup; the actual run button and live progress live in the **Tools** tab, with a shortcut button in the Sync sub-tab that jumps there. If audio is playing when a sync starts, playback is paused for the SD-writing phase and resumes automatically when the sync finishes; pressing play (or skipping a track) cancels the running sync and resumes playback immediately | [`ac24bbc`](../../commit/ac24bbc) |
136136
| **Fully offline web interface**: all third-party libraries (jQuery, jQuery-UI, Bootstrap JS/CSS, jstree + themes, bootstrap-slider, Font Awesome + webfonts, i18next) and the Orbitron/Rajdhani/Share Tech Mono fonts are now vendored and served gzipped from the device instead of public CDNs. This fixes the ~2-minute first-load hang with empty (untranslated) text after a cold boot/flash, and makes the management UI work without any internet access (incl. AP mode). `processHtml.py` embeds the assets into flash (build size grows accordingly) and now derives unique header symbol names from the full asset path. The libs are concatenated into a single JS and a single CSS bundle (`tools/bundle_assets.py`) so the page makes only a handful of requests — many parallel requests to the ESP32's async web server made some browsers (Safari/Chrome) stall for minutes while Firefox coped | [`c618191`](../../commit/c618191) |
137137
| **M3U playlist builder**: a "Create playlist" button in the Files tab opens a two-pane builder — browse the SD card on the left, an ordered (drag-to-reorder) track list on the right — to assemble an `.m3u` from SD files, whole folders, and webradio URLs (which `LOCAL_M3U` plays interchangeably). Saving writes `/Playlists/<name>.m3u` via a new `POST /playlist` endpoint; the file can then be assigned to an RFID tag with play mode 11. i18n de/en/fr | [`af041fc`](../../commit/af041fc) |
138+
| **OpenAPI/Swagger spec updated**: the bundled `REST_API.yaml` (served at `/swagger.html`) now documents all fork-added endpoints (`/sync`, `/syncstop`, `/playlist`, `/githubupdate`, `/version`, `/login`, `/logout`, `/security`, `/eqrule(s)`, `/activeequalizer`, `/bluetooth*`, `/sdclean`) and declares a `basicAuth` security scheme so the Swagger "Authorize" button works (username `espuino`) | [`PENDING`](../../commit/PENDING) |
138139

139140
#### Feature highlights
140141

‎REST-API.yaml‎

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ servers:
1212
description: Local Device
1313
- url: "http://espuino.local/"
1414
description: Development ESPuino
15+
# When a web password is set, every endpoint (except /login and a few PWA assets)
16+
# requires authentication. Two equivalent ways to authenticate:
17+
# * HTTP Basic Auth on each request — username "espuino", password = web password
18+
# (e.g. `curl -u espuino:<password> ...`). Use the "Authorize" button in Swagger.
19+
# * Cookie session — POST the password to /login, which sets the ESPUINO_SESSION cookie.
20+
# With no web password set, the API is open and authentication is ignored.
21+
components:
22+
securitySchemes:
23+
basicAuth:
24+
type: http
25+
scheme: basic
26+
description: 'HTTP Basic Auth. Username is always "espuino"; password is the web-interface password.'
27+
security:
28+
- basicAuth: []
1529
paths:
1630
/explorer:
1731
get:
@@ -581,3 +595,276 @@ paths:
581595
responses:
582596
"200":
583597
description: Successful response for initializing Hall Effect Sensor Value.
598+
# ----------------------------------------------------------------------------
599+
# Fork-specific endpoints (Leo Industries AT-1)
600+
# ----------------------------------------------------------------------------
601+
/sync:
602+
get:
603+
tags: [Fork]
604+
summary: Get HTTP file-sync status.
605+
description: Poll the progress/result of the HTTP(S) file sync.
606+
responses:
607+
"200":
608+
description: Current sync state.
609+
content:
610+
application/json:
611+
schema:
612+
type: object
613+
properties:
614+
status:
615+
type: integer
616+
description: "0=idle, 1=syncing, 2=done, 3=failed, 4=stopped"
617+
progress:
618+
type: integer
619+
description: Percent of files processed.
620+
message:
621+
type: string
622+
description: Current file (while syncing) or the result/error message.
623+
post:
624+
tags: [Fork]
625+
summary: Start an HTTP file sync.
626+
description: >-
627+
Starts a background sync that pulls the files listed in the configured
628+
manifest URL onto the SD card. No-op if a sync is already running. If
629+
audio is playing it is paused for the SD-writing phase.
630+
responses:
631+
"200":
632+
description: Sync started (or already running).
633+
/syncstop:
634+
post:
635+
tags: [Fork]
636+
summary: Stop the running file sync.
637+
description: Requests a cooperative cancel of the currently running sync.
638+
responses:
639+
"200":
640+
description: Stop requested.
641+
/playlist:
642+
post:
643+
tags: [Fork]
644+
summary: Create an .m3u playlist.
645+
description: >-
646+
Writes an .m3u playlist to the SD card from the given ordered list of
647+
SD-file paths and/or webradio URLs (played interchangeably by LOCAL_M3U).
648+
requestBody:
649+
required: true
650+
content:
651+
application/json:
652+
schema:
653+
type: object
654+
required: [path, tracks]
655+
properties:
656+
path:
657+
type: string
658+
description: Target path; ".m3u" is appended if missing (e.g. /Playlists/mix).
659+
tracks:
660+
type: array
661+
items:
662+
type: string
663+
description: Ordered track lines (SD paths or http(s) URLs).
664+
responses:
665+
"200":
666+
description: Playlist created.
667+
"400":
668+
description: Missing path or tracks.
669+
"500":
670+
description: File could not be created.
671+
/githubupdate:
672+
get:
673+
tags: [Fork]
674+
summary: Get GitHub OTA status.
675+
description: Poll the status of the GitHub firmware update.
676+
responses:
677+
"200":
678+
description: Current update status.
679+
post:
680+
tags: [Fork]
681+
summary: Start a GitHub OTA update.
682+
description: Downloads the rolling "latest" firmware from GitHub over HTTPS and flashes it.
683+
responses:
684+
"200":
685+
description: Update started.
686+
/version:
687+
get:
688+
tags: [Fork]
689+
summary: Get firmware version info.
690+
description: Returns the running build/rolling version and whether an update is available.
691+
responses:
692+
"200":
693+
description: Version info.
694+
content:
695+
application/json:
696+
schema:
697+
type: object
698+
additionalProperties: true
699+
/security:
700+
get:
701+
tags: [Fork]
702+
summary: Is web-interface password protection enabled?
703+
responses:
704+
"200":
705+
description: Whether a web password is set.
706+
content:
707+
application/json:
708+
schema:
709+
type: object
710+
properties:
711+
enabled:
712+
type: boolean
713+
post:
714+
tags: [Fork]
715+
summary: Set or clear the web-interface password.
716+
description: Sets a new web password (enables protection) or clears it when empty.
717+
requestBody:
718+
required: true
719+
content:
720+
application/json:
721+
schema:
722+
type: object
723+
properties:
724+
password:
725+
type: string
726+
responses:
727+
"200":
728+
description: Password updated.
729+
/login:
730+
post:
731+
tags: [Fork]
732+
summary: Log in to the web interface.
733+
description: >-
734+
Validates the password and sets the ESPUINO_SESSION cookie. Not needed
735+
when authenticating per-request via HTTP Basic Auth (user "espuino").
736+
requestBody:
737+
required: true
738+
content:
739+
application/json:
740+
schema:
741+
type: object
742+
properties:
743+
password:
744+
type: string
745+
responses:
746+
"200":
747+
description: Login successful; session cookie set.
748+
"401":
749+
description: Wrong password.
750+
"429":
751+
description: Temporarily locked after too many failed attempts.
752+
/logout:
753+
post:
754+
tags: [Fork]
755+
summary: Log out of the web interface.
756+
description: Clears the ESPUINO_SESSION cookie.
757+
responses:
758+
"200":
759+
description: Logged out.
760+
/eqrules:
761+
get:
762+
tags: [Fork]
763+
summary: List per-file/folder equalizer rules.
764+
responses:
765+
"200":
766+
description: Equalizer rules.
767+
content:
768+
application/json:
769+
schema:
770+
type: array
771+
items:
772+
type: object
773+
additionalProperties: true
774+
/eqrule:
775+
post:
776+
tags: [Fork]
777+
summary: Set a per-path equalizer rule.
778+
parameters:
779+
- in: query
780+
name: path
781+
required: true
782+
schema:
783+
type: string
784+
description: SD path (file or folder) the rule applies to.
785+
- in: query
786+
name: low
787+
schema:
788+
type: integer
789+
- in: query
790+
name: band
791+
schema:
792+
type: integer
793+
- in: query
794+
name: high
795+
schema:
796+
type: integer
797+
- in: query
798+
name: profile
799+
schema:
800+
type: string
801+
description: "Profile name (flat/music/speech/voiceBoost) or empty for custom bands."
802+
responses:
803+
"200":
804+
description: Rule stored.
805+
delete:
806+
tags: [Fork]
807+
summary: Delete a per-path equalizer rule.
808+
parameters:
809+
- in: query
810+
name: path
811+
required: true
812+
schema:
813+
type: string
814+
responses:
815+
"200":
816+
description: Rule deleted.
817+
/activeequalizer:
818+
get:
819+
tags: [Fork]
820+
summary: Get the active equalizer profile.
821+
responses:
822+
"200":
823+
description: Active equalizer settings.
824+
content:
825+
application/json:
826+
schema:
827+
type: object
828+
additionalProperties: true
829+
/bluetoothscan:
830+
get:
831+
tags: [Fork]
832+
summary: Start a Bluetooth device scan.
833+
responses:
834+
"200":
835+
description: Scan started.
836+
/bluetoothresults:
837+
get:
838+
tags: [Fork]
839+
summary: Get Bluetooth scan results.
840+
responses:
841+
"200":
842+
description: Discovered devices.
843+
content:
844+
application/json:
845+
schema:
846+
type: array
847+
items:
848+
type: string
849+
/bluetoothconnect:
850+
post:
851+
tags: [Fork]
852+
summary: Connect to a Bluetooth device.
853+
requestBody:
854+
required: true
855+
content:
856+
application/json:
857+
schema:
858+
type: object
859+
additionalProperties: true
860+
responses:
861+
"200":
862+
description: Connect requested.
863+
/sdclean:
864+
post:
865+
tags: [Fork]
866+
summary: Clean the SD card cache.
867+
description: Removes ESPuino's cached playlist/index files from the SD card.
868+
responses:
869+
"200":
870+
description: Cleaned.

‎src/Web.cpp‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,10 +452,19 @@ static void Web_RefreshSessionToken(void) {
452452
wwwSessionToken = Web_BuildSessionToken(password, salt);
453453
}
454454

455+
// Fixed username for HTTP Basic Auth; only the password (the configured web password)
456+
// is checked. Lets API clients and the Swagger UI authenticate per-request via the
457+
// Authorization header (e.g. `curl -u espuino:<password>`) instead of the cookie flow.
458+
static constexpr const char *wwwBasicAuthUser = "espuino";
459+
455460
static bool Web_IsAuthenticated(AsyncWebServerRequest *request) {
456461
if (wwwSessionToken.length() == 0) {
457462
return true;
458463
}
464+
// Accept the password sent directly via HTTP Basic Auth (username "espuino").
465+
if (request->authenticate(wwwBasicAuthUser, gPrefsSettings.getString("wwwPassword", "").c_str())) {
466+
return true;
467+
}
459468
if (!request->hasHeader("Cookie")) {
460469
return false;
461470
}

0 commit comments

Comments
 (0)