v2.2 - redesign admin panel, fix readme.md #58
@@ -5,13 +5,13 @@ Little nodejs + mongodb app that I hack together with ChatGPT and Cursor to keep
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
- User accounts: register, login, JWT auth
|
- User accounts: register, login, JWT auth
|
||||||
- Simple registration verification via `DOG_NAMES`
|
- Optional registration "verification" via `DOG_NAMES` (if unset, verification is skipped)
|
||||||
- Mark nominees as watched (per user), with watched date
|
- Mark nominees as watched (per user), with watched date
|
||||||
- Browse movies by Oscar year + API for available years
|
- Browse movies by Oscar year + API for available years
|
||||||
- User stats: watched count/ratio and watched titles
|
- User stats: watched count/ratio and watched titles
|
||||||
- Movie details pulled from OMDb (title/plot/rating/poster) via IMDb ID
|
- Movie details pulled from OMDb (title/plot/rating/poster) via IMDb ID (optional)
|
||||||
- Admin movie management: add, edit (optionally refresh from OMDb), delete
|
- Admin movie management: add, edit (optionally refresh from OMDb), delete
|
||||||
- Built-in player page with multiple source options (VOD link, direct video file, embed)
|
- Built-in player page with multiple source options (VOD link, direct video URL, embed, or server file)
|
||||||
- Resume playback: per-user playback progress saving (time/duration)
|
- Resume playback: per-user playback progress saving (time/duration)
|
||||||
- Global settings: active Oscar year, “completion” modal content for checklist
|
- Global settings: active Oscar year, “completion” modal content for checklist
|
||||||
- Password flows: admin temp password reset + forced change on next login
|
- Password flows: admin temp password reset + forced change on next login
|
||||||
@@ -35,7 +35,7 @@ MONGO_URI=mongodb://127.0.0.1:27017/oscars-pool # MongoDB connection string
|
|||||||
JWT_SECRET=replace-with-a-random-string # JWT signing secret (auth)
|
JWT_SECRET=replace-with-a-random-string # JWT signing secret (auth)
|
||||||
|
|
||||||
# optional
|
# optional
|
||||||
DOG_NAMES=woof,WOOF,Woof # simple registration "verification"
|
DOG_NAMES=woof,WOOF,Woof # registration verification answers (comma-separated)
|
||||||
OMDB_API=your_omdb_key # OMDb API key (movie info)
|
OMDB_API=your_omdb_key # OMDb API key (movie info)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -65,6 +65,10 @@ VIDEO_SESSION_MAX_AGE_SECONDS=28800 # video_auth cookie max-age (default: 8h)
|
|||||||
MOVIES_CACHE_TTL_MS=30000 # movies route cache TTL (default: 30000ms)
|
MOVIES_CACHE_TTL_MS=30000 # movies route cache TTL (default: 30000ms)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The server serves the UI from `public/` (open `http://localhost:5000` by default).
|
||||||
|
- If you use `video_file` in a movie record, it must be a **relative path** under `VIDEO_FILES_DIR` (the backend blocks traversal/absolute paths).
|
||||||
|
|
||||||
## Python video server (optional)
|
## Python video server (optional)
|
||||||
There is also a standalone Python server in `python_video_server/` that mirrors `/api/video/:id` (Range streaming + JWT + Mongo).
|
There is also a standalone Python server in `python_video_server/` that mirrors `/api/video/:id` (Range streaming + JWT + Mongo).
|
||||||
See `python_video_server/README.md` for full instructions. In addition to `MONGO_URI` and `JWT_SECRET`, you may need:
|
See `python_video_server/README.md` for full instructions. In addition to `MONGO_URI` and `JWT_SECRET`, you may need:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export CORS_ALLOW_ORIGINS="https://your-app.example.com" # default: "*"
|
|||||||
# Cookie behavior for /api/video/session
|
# Cookie behavior for /api/video/session
|
||||||
export VIDEO_COOKIE_SAMESITE="Lax" # Lax|Strict|None (default: Lax)
|
export VIDEO_COOKIE_SAMESITE="Lax" # Lax|Strict|None (default: Lax)
|
||||||
|
|
||||||
# Mongo TLS / connectivity troubleshooting (Atlas, weird hosts, etc.)
|
# Mongo TLS / connectivity troubleshooting (Atlas, odd hosts, etc.)
|
||||||
export MONGO_TLS_CA_FILE="/path/to/ca.pem" # prefer a valid CA bundle
|
export MONGO_TLS_CA_FILE="/path/to/ca.pem" # prefer a valid CA bundle
|
||||||
export MONGO_TLS_FORCE_TLS12="1" # force TLS 1.2 if TLS 1.3 breaks
|
export MONGO_TLS_FORCE_TLS12="1" # force TLS 1.2 if TLS 1.3 breaks
|
||||||
export MONGO_TLS_INSECURE="1" # NOT recommended; debug only
|
export MONGO_TLS_INSECURE="1" # NOT recommended; debug only
|
||||||
@@ -36,63 +36,104 @@ export MONGO_CONNECT_TIMEOUT_MS="8000"
|
|||||||
export MONGO_SOCKET_TIMEOUT_MS="20000"
|
export MONGO_SOCKET_TIMEOUT_MS="20000"
|
||||||
```
|
```
|
||||||
|
|
||||||
## HTTPS (Let’s Encrypt, Python-only)
|
## Video auth / usage
|
||||||
|
|
||||||
This server can be served over HTTPS by running `uvicorn` with `--ssl-certfile` and `--ssl-keyfile`.
|
`GET /api/video/{movieId}` accepts auth in three ways (in priority order):
|
||||||
To obtain/renew a trusted cert from Let’s Encrypt **without installing system certbot**, this repo includes a small wrapper that uses the **`certbot` Python package**.
|
|
||||||
|
|
||||||
### 1) Issue a certificate (HTTP-01 standalone)
|
1) `video_auth` cookie (best for browser `<video>` tags)
|
||||||
|
2) `Authorization: Bearer <token>` header
|
||||||
|
3) `?token=<token>` query param (debug only; tokens in URLs leak via logs/referrers)
|
||||||
|
|
||||||
Requirements:
|
### Browser `<video>` (cookie flow)
|
||||||
- Your domain’s DNS `A/AAAA` records must point to this machine.
|
|
||||||
- Port **80** must be reachable from the internet for the ACME challenge.
|
Because HTML `<video>` cannot send an `Authorization` header, the pattern is:
|
||||||
|
|
||||||
|
1. Your JS calls `POST /api/video/session` with `Authorization: Bearer <token>`
|
||||||
|
2. The server responds `204` and sets a `video_auth` cookie (scoped to `/api/video`)
|
||||||
|
3. Your `<video>` loads `/api/video/{movieId}` and the cookie is used automatically
|
||||||
|
|
||||||
|
Example (curl):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m pip install -r python_video_server/requirements.txt
|
# 1) exchange header token -> cookie
|
||||||
|
curl -i -X POST "https://video.example.com/api/video/session" \
|
||||||
|
-H "Authorization: Bearer $JWT"
|
||||||
|
|
||||||
# Staging by default (recommended first). Add --production when it works.
|
# 2) then the <video> element can simply use:
|
||||||
sudo -E python3 -m python_video_server.letsencrypt certonly \
|
# https://video.example.com/api/video/<movieId>
|
||||||
--email [email protected] \
|
|
||||||
--domains example.com,www.example.com
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1b) Issue a certificate (DNS-01 manual TXT record — no port 80, no root)
|
Cross-site note (frontend on a different origin):
|
||||||
|
- Set `CORS_ALLOW_ORIGINS` to your exact app origin (not `*`) and ensure your frontend uses `credentials: "include"` on the `/api/video/session` request.
|
||||||
|
- If the video host is cross-site, you’ll usually need `VIDEO_COOKIE_SAMESITE=None` **and** HTTPS, otherwise browsers may block the cookie.
|
||||||
|
|
||||||
If your host already uses port **80** (or you don't have permission to bind it), use DNS-01.
|
### Non-browser clients (header flow)
|
||||||
This does **not** require any inbound ports on the machine, but you must be able to edit DNS for the domain.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m pip install -r python_video_server/requirements.txt
|
curl -I "https://video.example.com/api/video/<movieId>" \
|
||||||
|
-H "Authorization: Bearer $JWT"
|
||||||
# Add --production when staging works.
|
|
||||||
python3 -m python_video_server.letsencrypt certonly \
|
|
||||||
--challenge dns \
|
|
||||||
--email [email protected] \
|
|
||||||
--domains example.com,www.example.com
|
|
||||||
```
|
```
|
||||||
|
|
||||||
During issuance, certbot will prompt you to create a TXT record like:
|
### Token in URL (debug flow)
|
||||||
- Name: `_acme-challenge.example.com`
|
|
||||||
- Type: `TXT`
|
|
||||||
- Value: (a token certbot prints)
|
|
||||||
|
|
||||||
After it propagates, press Enter and certbot will continue.
|
|
||||||
|
|
||||||
If you run into a situation where the prompt/token only shows up in logs, you can try the hook-based mode:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m python_video_server.letsencrypt certonly \
|
curl -I "https://video.example.com/api/video/<movieId>?token=$JWT"
|
||||||
--challenge dns \
|
|
||||||
--dns-hooks \
|
|
||||||
--email [email protected] \
|
|
||||||
--domains example.com,www.example.com
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Certificates will be written under:
|
### Seeking / Range requests (example)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i "https://video.example.com/api/video/<movieId>" \
|
||||||
|
-H "Authorization: Bearer $JWT" \
|
||||||
|
-H "Range: bytes=0-1023"
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTPS (optional)
|
||||||
|
|
||||||
|
You can run behind a reverse proxy/ingress, or run `uvicorn` with TLS directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn python_video_server.server:app --host 0.0.0.0 --port 443 \
|
||||||
|
--ssl-certfile /path/to/fullchain.pem \
|
||||||
|
--ssl-keyfile /path/to/privkey.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- If you’re behind a proxy, forward `X-Forwarded-Proto: https` so `/api/video/session` can set `Secure` cookies correctly.
|
||||||
|
|
||||||
|
## Let’s Encrypt certs (python-only helper)
|
||||||
|
|
||||||
|
This repo includes `python_video_server/letsencrypt.py`, a wrapper around the **certbot Python package** (no system `certbot` needed).
|
||||||
|
|
||||||
|
Certificates (and certbot state) are stored under:
|
||||||
- `python_video_server/certs/config/live/<primary-domain>/fullchain.pem`
|
- `python_video_server/certs/config/live/<primary-domain>/fullchain.pem`
|
||||||
- `python_video_server/certs/config/live/<primary-domain>/privkey.pem`
|
- `python_video_server/certs/config/live/<primary-domain>/privkey.pem`
|
||||||
|
|
||||||
### 2) Run the video server on 443 with TLS
|
You can override the storage directory with `LETSENCRYPT_BASE_DIR` (optional).
|
||||||
|
|
||||||
|
### Issue a cert (HTTP-01 standalone, easiest)
|
||||||
|
|
||||||
|
Requirements:
|
||||||
|
- DNS `A/AAAA` records point to this host
|
||||||
|
- Port **80** reachable from the internet
|
||||||
|
- You can bind port 80 (often requires `sudo`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install -r python_video_server/requirements.txt
|
||||||
|
|
||||||
|
# staging by default (recommended first)
|
||||||
|
sudo -E python -m python_video_server.letsencrypt certonly \
|
||||||
|
--email [email protected] \
|
||||||
|
--domains example.com,www.example.com
|
||||||
|
|
||||||
|
# production (real cert)
|
||||||
|
sudo -E python -m python_video_server.letsencrypt certonly \
|
||||||
|
--email [email protected] \
|
||||||
|
--domains example.com,www.example.com \
|
||||||
|
--production
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run uvicorn with those certs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -E uvicorn python_video_server.server:app --host 0.0.0.0 --port 443 \
|
sudo -E uvicorn python_video_server.server:app --host 0.0.0.0 --port 443 \
|
||||||
@@ -100,18 +141,28 @@ sudo -E uvicorn python_video_server.server:app --host 0.0.0.0 --port 443 \
|
|||||||
--ssl-keyfile python_video_server/certs/config/live/example.com/privkey.pem
|
--ssl-keyfile python_video_server/certs/config/live/example.com/privkey.pem
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
### Issue a cert (DNS-01 manual TXT, no port 80)
|
||||||
- Binding to ports **80/443** usually requires `sudo` (or a reverse proxy / port forwarding).
|
|
||||||
- If you’re behind a proxy/ingress, keep sending `X-Forwarded-Proto: https` so `/api/video/session` sets `Secure` cookies correctly.
|
|
||||||
|
|
||||||
### 3) Renew certificates
|
Use this if you cannot expose/bind port 80. Certbot will prompt you to create a TXT record.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Dry-run renewal (safe)
|
python -m pip install -r python_video_server/requirements.txt
|
||||||
sudo -E python3 -m python_video_server.letsencrypt renew --dry-run
|
|
||||||
|
|
||||||
# Actual renew (typically run via cron/systemd timer)
|
python -m python_video_server.letsencrypt certonly \
|
||||||
sudo -E python3 -m python_video_server.letsencrypt renew --production
|
--challenge dns \
|
||||||
|
--email [email protected] \
|
||||||
|
--domains example.com,www.example.com \
|
||||||
|
--production
|
||||||
|
```
|
||||||
|
|
||||||
|
### Renew certs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# dry-run (safe)
|
||||||
|
sudo -E python -m python_video_server.letsencrypt renew --dry-run
|
||||||
|
|
||||||
|
# real renewal
|
||||||
|
sudo -E python -m python_video_server.letsencrypt renew --production
|
||||||
```
|
```
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|||||||
Reference in New Issue
Block a user