mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
feat(self-hosted): server storage API in web-backend (#1898)
* feat(interfaces): add the server storage HTTP contract Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): read server storage configuration Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): open the server database and track revisions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): playlist and user-state repositories Playlist documents use the shared desktop row codec, so the server writes the same rows. keepPayload upserts take the stored playlist/items/header fields when the client leaves them out. Xtream user data lives in the server-only server_user_state table, one JSON value per namespace and playlist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): storage error mapping and access token guard Storage errors map to JSON status bodies and are logged by name and code only. An optional bearer token guards every route except /, /health, /config.js and /v1/instance, provider proxy routes included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): serve the /v1 server storage API /v1/instance is always public and reports whether server storage is on. With storage, /v1/playlists serves whole documents with ETags and 304s, accepts keepPayload upserts, and deletes playlists with their user state; /v1/user-state and /v1/app-state store the rest. The proxy routes are unchanged unless an access token is configured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(web-backend): open server storage on start and close on shutdown With IPTVNATOR_STORAGE=server, main.ts opens the database before it listens and exits with a clear message when the data directory is not writable. SIGTERM and SIGINT close the server and the database. The native better-sqlite3 module stays outside the esbuild bundle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): match weakened ETags on playlist reads nginx weakens the ETag of a gzipped response, and clients send the weak form back in If-None-Match. The route now compares tags weakly, as RFC 9110 requires for GET, so it answers 304 before reading the document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(docker): persist server storage on a volume The image builds better-sqlite3 in a native stage per target platform, enables server storage on the /data volume, and raises the nginx body limit to 64 MB for /api with gzip for JSON. The compose file mounts a named volume and documents the optional access token. The PWA does not use the storage yet; docs say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(web-backend): type the storage route responses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): harden server storage reads, deletes and app state - Metadata reads return the stored document without its payload fields, so fields that live only in the JSON survive a metadata round-trip. - A conditional GET checks that the playlist exists before answering 304. - Deleting a playlist and its user state commits in one transaction; the ETag moves only after the commit. - Browser app state is stored under a client: prefix and the instance id is repaired when empty, so a client write cannot stop the backend. - User-state buckets have no prototype, so ids like __proto__ round-trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(self-hosted): keep the access token unset until the PWA sends it Add the release note for the Docker server storage volume. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): bound shutdown and reject padded playlist ids - SIGTERM closes idle connections at once and cuts the rest after five seconds, so a long provider download cannot keep the backend running. - Playlist ids with surrounding whitespace are rejected: the row codec trims ids, so storage and ETags would track different keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): clear optional columns on whole-document upserts The row codec maps empty values to undefined, which ON CONFLICT DO UPDATE skips, so a cleared User-Agent, Referrer or credential kept its old column value and came back on the next read. Server upserts now write NULL to every replaceable column the document no longer sets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(self-hosted): linear token parsing, no-store app state, longer start - The access guard reads the Bearer token by string scanning instead of a regular expression that CodeQL flagged as polynomial on long whitespace. - GET /v1/app-state/:key sends Cache-Control: no-store like the other state routes, so devices never reuse a stale value. - The entrypoint waits up to IPTVNATOR_BACKEND_START_TIMEOUT (300 s) for /health, because the backend migrates its database before it listens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(self-hosted): metadata reads skip payloads, redact JSON parse warnings List playlist metadata from a server-only payload-free copy of each document instead of running json_remove over every payload blob, and log only the error name when a stored JSON value fails to parse, since the parser's message quotes the stored text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-backend): copy carried-over panel clock into playlist metadata When an upsert keeps the stored serverTimezone, build the metadata copy from the saved payload so metadata and full reads agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e9795ac54
commit
b88a1c81e5
41 files changed
+2312
-16
No files matched your search
@@ -73,6 +73,40 @@ The current self-hosted PWA uses these `apps/web-backend` routes:
|
||||
- `GET /xtream?targetId=<id>&username=<u>&password=<p>&action=<action>`
|
||||
- `GET /stalker?targetId=<id>&macAddress=<mac>&action=<action>`
|
||||
|
||||
Server storage adds versioned routes under `/v1`; the PWA does not call them
|
||||
yet:
|
||||
|
||||
- `GET /v1/instance` (always public): `{ service, apiVersion, instanceId,
|
||||
storage: 'server' | 'none', auth: 'none' | 'token' }`
|
||||
- `GET /v1/instance/status`: playlist count, database size and path
|
||||
- `GET /v1/playlists[?view=meta]`, `GET|PUT|DELETE /v1/playlists/:id`,
|
||||
`DELETE /v1/playlists`: whole playlist documents; `GET` sends an `ETag`
|
||||
and answers `304` to a matching `If-None-Match`, and
|
||||
`PUT ...?keepPayload=1` keeps the stored `playlist`, `items` and
|
||||
`header` fields (`409 payload_missing` without a stored row)
|
||||
- `GET /v1/user-state`, `PUT|DELETE /v1/user-state/:namespace/:playlistId`:
|
||||
Xtream favorites, recently viewed, collection snapshots and playback
|
||||
positions, one JSON value per namespace and playlist
|
||||
- `GET|PUT /v1/app-state/:key`
|
||||
|
||||
Server storage lives in `apps/web-backend/src/app/storage/`. With
|
||||
`IPTVNATOR_STORAGE=server`, `main.ts` opens the shared database through
|
||||
`initDatabase()` (path from `IPTVNATOR_DATA_DIR`), so the server has the same
|
||||
schema and migrations as the desktop app and writes playlists with the
|
||||
shared row codec. It adds two server-only tables: `server_user_state` for
|
||||
Xtream user data, and `server_playlist_meta`, a copy of each document
|
||||
without its M3U payload fields that metadata reads use instead of walking
|
||||
the payload blobs (filled once on open for rows written without it). ETags
|
||||
come from an in-memory revision counter; the server is the only writer of
|
||||
its database. Storage errors are logged by error name and code only, because
|
||||
Drizzle messages can carry bound values such as portal credentials. With
|
||||
`IPTVNATOR_ACCESS_TOKEN`, every route except `/`, `/health`, `/config.js`
|
||||
and `/v1/instance` requires `Authorization: Bearer <token>`, the proxy
|
||||
routes included. The PWA cannot send that token yet, so the variable must
|
||||
stay unset until the browser side lands. Browser app state written through
|
||||
`/v1/app-state` is stored under a `client:` key prefix, apart from
|
||||
server-owned keys such as the instance id.
|
||||
|
||||
`pnpm nx run web-backend:serve` starts the backend by hand on port 3333 with
|
||||
`TSX_TSCONFIG_PATH=tsconfig.base.json`, which `tsx` needs for the
|
||||
`@iptvnator/*` path aliases. `apps/web-e2e/playwright.config.ts` does not use
|
||||
@@ -274,6 +308,15 @@ Default runtime values:
|
||||
- `PORT=3000`
|
||||
- `IPTVNATOR_PROXY_ALLOW_PRIVATE_NETWORKS=0`
|
||||
- `NODE_EXTRA_CA_CERTS` unset
|
||||
- `IPTVNATOR_STORAGE=server`, `IPTVNATOR_DATA_DIR=/data` (a `VOLUME`)
|
||||
- `IPTVNATOR_ACCESS_TOKEN` unset, `IPTVNATOR_MAX_DOCUMENT_MB=64`
|
||||
- `IPTVNATOR_BACKEND_START_TIMEOUT=300`: seconds the entrypoint waits for
|
||||
`/health`; the backend migrates its database before it listens
|
||||
|
||||
better-sqlite3 is a native addon, so the Dockerfile builds it in a separate
|
||||
`native` stage that is not pinned to `$BUILDPLATFORM`; multi-arch builds
|
||||
produce it for each target platform. nginx raises `client_max_body_size` to
|
||||
64 MB for `/api/` and gzips JSON responses.
|
||||
|
||||
When hosting behind another domain, set `CLIENT_URL` to the browser origin and
|
||||
keep `BACKEND_URL=/api` unless the reverse proxy exposes the backend elsewhere.
|
||||
@@ -288,6 +331,7 @@ Use the narrow validation ladder for self-hosted changes:
|
||||
|
||||
```bash
|
||||
pnpm nx test web-backend
|
||||
pnpm nx test web-backend --testPathPatterns=storage
|
||||
pnpm nx test web --runTestsByPath apps/web/src/app/services/runtime-config.spec.ts
|
||||
pnpm nx build web --configuration=pwa --skip-nx-cache
|
||||
pnpm nx build web-backend
|
||||
|
||||
Reference in new issue
Block a user