2026-06-04 eagle-dashboard: add auth, FileBrowser, Pages tab, cloudflared tunnel
This commit is contained in:
@@ -4,17 +4,17 @@
|
|||||||
|
|
||||||
**Goal:** Replace `localhost` with a visual control panel that shows health, enables management (start/stop/restart) of all 25+ Eagle LaunchAgent services, and displays network security posture per service.
|
**Goal:** Replace `localhost` with a visual control panel that shows health, enables management (start/stop/restart) of all 25+ Eagle LaunchAgent services, and displays network security posture per service.
|
||||||
|
|
||||||
**Architecture:** FastAPI backend reads a YAML service registry and checks health via HTTP/port/process checks every 30s. HTMX + Tailwind CDN frontend renders live cards with no build step. `launchctl` integration enables lifecycle control. Runs as its own LaunchAgent on port 3000.
|
**Architecture:** FastAPI backend reads a YAML service registry and checks health via HTTP/port/process checks every 30s. HTMX + Tailwind CDN frontend with three tabs: **Services** (health cards + control), **Pages** (local page tester — URL input + iframe), **Files** (embedded FileBrowser). Origin-aware auth middleware: `127.0.0.1` → free access; external via cloudflared `dash.qentra.top` → Bearer token required. LaunchAgent on `:3000`. FileBrowser Docker container on `:8181`.
|
||||||
|
|
||||||
**Tech Stack:** Python 3.11, FastAPI, uvicorn, HTMX 2, Tailwind CDN, Alpine.js, PyYAML, psutil, httpx
|
**Tech Stack:** Python 3.11, FastAPI, uvicorn, HTMX 2, Tailwind CDN, Alpine.js, PyYAML, psutil, httpx, python-dotenv
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Scope note
|
## Scope note
|
||||||
|
|
||||||
Two independent subsystems — build them separately:
|
Three independent subsystems:
|
||||||
- **Plan A (this doc):** Service registry + health API + dashboard UI + LaunchAgent control
|
- **Plan A (this doc):** Service registry + health API + dashboard UI + LaunchAgent control + auth + FileBrowser + Pages tab + cloudflared tunnel
|
||||||
- **Plan B (future):** Network security panel — cloudflared tunnel management, pf rules, per-service firewall toggling
|
- **Plan B (future):** Network security panel — pf rules, per-service firewall toggling, bind-address runtime verification
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,12 +23,16 @@ Two independent subsystems — build them separately:
|
|||||||
```
|
```
|
||||||
~/Developer/eagle-dash/
|
~/Developer/eagle-dash/
|
||||||
├── main.py # FastAPI app — routes, startup, SSE
|
├── main.py # FastAPI app — routes, startup, SSE
|
||||||
|
├── auth.py # Origin-aware auth middleware
|
||||||
├── registry.py # Service registry loader + YAML parser
|
├── registry.py # Service registry loader + YAML parser
|
||||||
├── health.py # Health check engine (http/port/process/launchctl)
|
├── health.py # Health check engine (http/port/process/launchctl)
|
||||||
├── control.py # launchctl start/stop/restart wrappers
|
├── control.py # launchctl start/stop/restart wrappers
|
||||||
├── services.yaml # Service definitions (source of truth)
|
├── services.yaml # Service definitions (source of truth)
|
||||||
|
├── .env # EAGLE_DASH_TOKEN=<secret>
|
||||||
├── templates/
|
├── templates/
|
||||||
│ └── index.html # Dashboard — HTMX + Tailwind CDN + Alpine.js
|
│ └── index.html # Dashboard — 3-tab HTMX + Tailwind CDN + Alpine.js
|
||||||
|
├── docker/
|
||||||
|
│ └── filebrowser.yml # FileBrowser Docker Compose
|
||||||
├── requirements.txt
|
├── requirements.txt
|
||||||
├── justfile
|
├── justfile
|
||||||
└── com.eagle.dashboard.plist # LaunchAgent plist
|
└── com.eagle.dashboard.plist # LaunchAgent plist
|
||||||
@@ -286,6 +290,105 @@ git add services.yaml && git commit -m "feat: service registry YAML"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Task 2a: Auth Middleware
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/Developer/eagle-dash/auth.py`
|
||||||
|
- Create: `~/Developer/eagle-dash/.env`
|
||||||
|
|
||||||
|
Origin-aware authentication: requests from `127.0.0.1` (localhost browser) bypass all auth. Requests arriving via cloudflared (any other origin or forwarded header) must present a Bearer token matching `EAGLE_DASH_TOKEN` env var.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Generate a token and write .env**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Developer/eagle-dash
|
||||||
|
python3 -c "import secrets; print('EAGLE_DASH_TOKEN=' + secrets.token_urlsafe(32))" > .env
|
||||||
|
cat .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `EAGLE_DASH_TOKEN=<48-char random string>`
|
||||||
|
|
||||||
|
Add `.env` to `.gitignore`:
|
||||||
|
```bash
|
||||||
|
echo ".env" >> .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write auth.py**
|
||||||
|
|
||||||
|
`~/Developer/eagle-dash/auth.py`:
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
from fastapi import Request, HTTPException
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
TOKEN = os.getenv("EAGLE_DASH_TOKEN", "")
|
||||||
|
|
||||||
|
LOCALHOST_HOSTS = {"127.0.0.1", "::1", "localhost"}
|
||||||
|
|
||||||
|
class OriginAuthMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
client_host = (request.client.host if request.client else "")
|
||||||
|
forwarded_for = request.headers.get("X-Forwarded-For", "")
|
||||||
|
|
||||||
|
is_local = (
|
||||||
|
client_host in LOCALHOST_HOSTS
|
||||||
|
and not forwarded_for
|
||||||
|
)
|
||||||
|
if is_local:
|
||||||
|
return await call_next(request)
|
||||||
|
|
||||||
|
# External request — require Bearer token
|
||||||
|
auth = request.headers.get("Authorization", "")
|
||||||
|
if not TOKEN:
|
||||||
|
return JSONResponse({"error": "token not configured"}, status_code=503)
|
||||||
|
if auth != f"Bearer {TOKEN}":
|
||||||
|
return JSONResponse({"error": "unauthorized"}, status_code=401)
|
||||||
|
return await call_next(request)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Wire into main.py**
|
||||||
|
|
||||||
|
Add to `main.py` imports and app setup (before the first route):
|
||||||
|
```python
|
||||||
|
from auth import OriginAuthMiddleware
|
||||||
|
# after: app = FastAPI(...)
|
||||||
|
app.add_middleware(OriginAuthMiddleware)
|
||||||
|
```
|
||||||
|
|
||||||
|
Also add `python-dotenv` to `requirements.txt`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Test locally**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Developer/eagle-dash
|
||||||
|
just dev &
|
||||||
|
sleep 2
|
||||||
|
# Should work (localhost):
|
||||||
|
curl -s http://127.0.0.1:3000/api/health
|
||||||
|
|
||||||
|
# Simulate external (should 401):
|
||||||
|
curl -s -H "X-Forwarded-For: 1.2.3.4" http://127.0.0.1:3000/api/health
|
||||||
|
|
||||||
|
# With token (should work):
|
||||||
|
TOKEN=$(grep EAGLE_DASH_TOKEN .env | cut -d= -f2)
|
||||||
|
curl -s -H "X-Forwarded-For: 1.2.3.4" -H "Authorization: Bearer $TOKEN" http://127.0.0.1:3000/api/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: first call `{"status":"ok"}`, second `{"error":"unauthorized"}`, third `{"status":"ok"}`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Developer/eagle-dash
|
||||||
|
git add auth.py .gitignore requirements.txt main.py
|
||||||
|
git commit -m "feat: origin-aware auth middleware — localhost free, external token-gated"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Task 3: Registry Loader
|
## Task 3: Registry Loader
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
@@ -704,7 +807,7 @@ git add main.py control.py && git commit -m "feat: FastAPI backend with health A
|
|||||||
**Files:**
|
**Files:**
|
||||||
- Create: `~/Developer/eagle-dash/templates/index.html`
|
- Create: `~/Developer/eagle-dash/templates/index.html`
|
||||||
|
|
||||||
The UI uses Tailwind CDN (no build step), HTMX for polling, Alpine.js for interactivity.
|
The UI uses Tailwind CDN (no build step), HTMX for polling, Alpine.js for interactivity. Three tabs: **Services**, **Pages**, **Files**.
|
||||||
|
|
||||||
- [ ] **Step 1: Write index.html**
|
- [ ] **Step 1: Write index.html**
|
||||||
|
|
||||||
@@ -872,6 +975,87 @@ The UI uses Tailwind CDN (no build step), HTMX for polling, Alpine.js for intera
|
|||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The `<body>` element uses `x-data="dashboard()"` and tab state `activeTab: 'services'`. Add a tab bar at the top:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Tab Bar -->
|
||||||
|
<div class="flex gap-1 mb-6 border-b border-slate-700 pb-2">
|
||||||
|
<button @click="activeTab='services'"
|
||||||
|
:class="activeTab==='services' ? 'text-white border-b-2 border-indigo-500' : 'text-slate-500 hover:text-slate-300'"
|
||||||
|
class="px-4 py-1.5 text-sm font-medium -mb-2">Services</button>
|
||||||
|
<button @click="activeTab='pages'"
|
||||||
|
:class="activeTab==='pages' ? 'text-white border-b-2 border-indigo-500' : 'text-slate-500 hover:text-slate-300'"
|
||||||
|
class="px-4 py-1.5 text-sm font-medium -mb-2">Pages</button>
|
||||||
|
<button @click="activeTab='files'"
|
||||||
|
:class="activeTab==='files' ? 'text-white border-b-2 border-indigo-500' : 'text-slate-500 hover:text-slate-300'"
|
||||||
|
class="px-4 py-1.5 text-sm font-medium -mb-2">Files</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Wrap the existing category grid in `<div x-show="activeTab==='services'">`.
|
||||||
|
|
||||||
|
Add a **Pages tab** section:
|
||||||
|
```html
|
||||||
|
<!-- Pages Tab -->
|
||||||
|
<div x-show="activeTab==='pages'" class="flex flex-col gap-4 h-[80vh]">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input x-model="pageUrl" @keyup.enter="loadPage()"
|
||||||
|
placeholder="http://localhost:8080/..."
|
||||||
|
class="flex-1 bg-slate-800 border border-slate-600 rounded px-3 py-1.5 text-sm text-white
|
||||||
|
font-mono placeholder-slate-500 focus:outline-none focus:border-indigo-500" />
|
||||||
|
<button @click="loadPage()"
|
||||||
|
class="px-4 py-1.5 bg-indigo-600 hover:bg-indigo-500 rounded text-sm text-white">Load</button>
|
||||||
|
<button @click="pageUrl=''; pageLoaded=false"
|
||||||
|
class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 rounded text-sm text-slate-300">Clear</button>
|
||||||
|
</div>
|
||||||
|
<!-- Quick links to common local services -->
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<template x-for="link in quickLinks" :key="link.url">
|
||||||
|
<button @click="pageUrl=link.url; loadPage()"
|
||||||
|
class="text-xs px-2 py-1 bg-slate-800 hover:bg-slate-700 border border-slate-600
|
||||||
|
rounded text-slate-400 hover:text-white">
|
||||||
|
<span x-text="link.label"></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<iframe x-show="pageLoaded" :src="pageUrl" class="flex-1 rounded border border-slate-700 bg-white"
|
||||||
|
sandbox="allow-same-origin allow-scripts allow-forms"></iframe>
|
||||||
|
<div x-show="!pageLoaded" class="flex-1 rounded border border-slate-700 flex items-center
|
||||||
|
justify-center text-slate-600 text-sm">
|
||||||
|
Enter a URL above to load a page
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a **Files tab** section:
|
||||||
|
```html
|
||||||
|
<!-- Files Tab -->
|
||||||
|
<div x-show="activeTab==='files'" class="h-[80vh]">
|
||||||
|
<iframe src="http://localhost:8181"
|
||||||
|
class="w-full h-full rounded border border-slate-700"
|
||||||
|
allow="fullscreen"></iframe>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to the `dashboard()` Alpine object:
|
||||||
|
```javascript
|
||||||
|
activeTab: 'services',
|
||||||
|
pageUrl: '',
|
||||||
|
pageLoaded: false,
|
||||||
|
quickLinks: [
|
||||||
|
{ label: 'Hermes :8000', url: 'http://localhost:8000' },
|
||||||
|
{ label: 'Postgres :5432', url: 'http://localhost:5432' },
|
||||||
|
{ label: 'MLX LM :8080', url: 'http://localhost:8080/v1/models' },
|
||||||
|
{ label: 'Ollama :11434', url: 'http://localhost:11434' },
|
||||||
|
{ label: 'Zulip :8080', url: 'http://localhost:8080' },
|
||||||
|
],
|
||||||
|
loadPage() {
|
||||||
|
if (!this.pageUrl) return;
|
||||||
|
this.pageLoaded = false;
|
||||||
|
this.$nextTick(() => { this.pageLoaded = true; });
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
- [ ] **Step 2: Open the dashboard in browser**
|
- [ ] **Step 2: Open the dashboard in browser**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -880,7 +1064,7 @@ just dev
|
|||||||
open http://localhost:3000
|
open http://localhost:3000
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: Dark dashboard with service cards grouped by category, green/red dots, network badges, restart/stop buttons.
|
Expected: Dark dashboard with tab bar. Services tab shows cards by category. Pages tab shows URL input with iframe. Files tab shows FileBrowser.
|
||||||
|
|
||||||
- [ ] **Step 3: Verify all categories appear**
|
- [ ] **Step 3: Verify all categories appear**
|
||||||
|
|
||||||
@@ -890,7 +1074,7 @@ Expected categories: `ai`, `database`, `monitoring`, `orchestration`, `infra`
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/Developer/eagle-dash
|
cd ~/Developer/eagle-dash
|
||||||
git add templates/index.html && git commit -m "feat: dashboard UI with HTMX, Tailwind, Alpine.js"
|
git add templates/index.html && git commit -m "feat: 3-tab UI — Services, Pages, Files"
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1062,6 +1246,191 @@ Expected: Dashboard loads, eagle-dashboard itself shows as a green card.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Task 9: FileBrowser Docker Container
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/Developer/eagle-dash/docker/filebrowser.yml`
|
||||||
|
|
||||||
|
FileBrowser is a web UI for browsing and managing files — same image used on TrueNAS. Runs via Colima (Docker on Mac).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write docker-compose file**
|
||||||
|
|
||||||
|
`~/Developer/eagle-dash/docker/filebrowser.yml`:
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
filebrowser:
|
||||||
|
image: filebrowser/filebrowser:latest
|
||||||
|
container_name: eagle-filebrowser
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8181:80"
|
||||||
|
volumes:
|
||||||
|
- /Users/admin:/srv
|
||||||
|
- filebrowser_db:/database
|
||||||
|
environment:
|
||||||
|
- FB_NOAUTH=true # auth handled by Eagle Dashboard token gate
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
filebrowser_db:
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `FB_NOAUTH=true` disables FileBrowser's own auth since it's served behind Eagle Dashboard's token middleware (external requests already authenticated before they reach the iframe).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Start it**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f ~/Developer/eagle-dash/docker/filebrowser.yml up -d
|
||||||
|
curl -s http://localhost:8181
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FileBrowser HTML response.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add to services.yaml**
|
||||||
|
|
||||||
|
Under `# ── Infrastructure`:
|
||||||
|
```yaml
|
||||||
|
filebrowser:
|
||||||
|
name: "FileBrowser"
|
||||||
|
label: "eagle-filebrowser"
|
||||||
|
port: 8181
|
||||||
|
health:
|
||||||
|
type: http
|
||||||
|
url: "http://localhost:8181"
|
||||||
|
access: localhost
|
||||||
|
critical: false
|
||||||
|
category: infra
|
||||||
|
log: ~
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add docker start to justfile**
|
||||||
|
|
||||||
|
```just
|
||||||
|
filebrowser:
|
||||||
|
docker compose -f docker/filebrowser.yml up -d
|
||||||
|
|
||||||
|
filebrowser-stop:
|
||||||
|
docker compose -f docker/filebrowser.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Make it start at login via LaunchAgent**
|
||||||
|
|
||||||
|
Create `~/Library/LaunchAgents/com.eagle.filebrowser.plist`:
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key><string>com.eagle.filebrowser</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/local/bin/docker</string>
|
||||||
|
<string>compose</string>
|
||||||
|
<string>-f</string>
|
||||||
|
<string>/Users/admin/Developer/eagle-dash/docker/filebrowser.yml</string>
|
||||||
|
<string>up</string>
|
||||||
|
<string>-d</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key><true/>
|
||||||
|
<key>StandardOutPath</key><string>/tmp/eagle-filebrowser.log</string>
|
||||||
|
<key>StandardErrorPath</key><string>/tmp/eagle-filebrowser.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
launchctl load ~/Library/LaunchAgents/com.eagle.filebrowser.plist
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Developer/eagle-dash
|
||||||
|
git add docker/ services.yaml justfile
|
||||||
|
git commit -m "feat: FileBrowser Docker container on :8181"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10: Cloudflared Tunnel for dash.qentra.top
|
||||||
|
|
||||||
|
**Goal:** Expose `dash.qentra.top` via cloudflared → `localhost:3000`. External requests are token-gated by auth middleware (Task 2a).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add ingress rule to cloudflared config**
|
||||||
|
|
||||||
|
Check existing config:
|
||||||
|
```bash
|
||||||
|
cat ~/.cloudflared/config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a new ingress entry for the dashboard:
|
||||||
|
```yaml
|
||||||
|
ingress:
|
||||||
|
- hostname: dash.qentra.top
|
||||||
|
service: http://localhost:3000
|
||||||
|
# ... existing rules ...
|
||||||
|
- service: http_status:404
|
||||||
|
```
|
||||||
|
|
||||||
|
If a `tunnel:` + `credentials-file:` block already exists, just add the ingress line above any catch-all rule.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Restart cloudflared**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
launchctl kickstart -k gui/$(id -u)/com.cloudflare.cloudflared
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if it's a brew service:
|
||||||
|
```bash
|
||||||
|
brew services restart cloudflare/cloudflare/cloudflared
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Create DNS CNAME in Cloudflare dashboard**
|
||||||
|
|
||||||
|
In Cloudflare DNS for `qentra.top`:
|
||||||
|
- Type: `CNAME`
|
||||||
|
- Name: `dash`
|
||||||
|
- Target: `<your-tunnel-id>.cfargotunnel.com`
|
||||||
|
- Proxy: enabled (orange cloud)
|
||||||
|
|
||||||
|
- [ ] **Step 4: Test external access**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TOKEN=$(grep EAGLE_DASH_TOKEN ~/Developer/eagle-dash/.env | cut -d= -f2)
|
||||||
|
curl -s https://dash.qentra.top/api/health # should 401
|
||||||
|
curl -s -H "Authorization: Bearer $TOKEN" https://dash.qentra.top/api/health # should 200
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add bookmarklet for browser access**
|
||||||
|
|
||||||
|
For convenient external access from a browser, create a bookmarklet that injects the token header. Or simpler — add a `/login` page to FastAPI that sets a cookie:
|
||||||
|
|
||||||
|
`~/Developer/eagle-dash/auth.py` extension: check `eagle_token` cookie in addition to `Authorization` header. The `/login?token=<TOKEN>` route sets the cookie and redirects to `/`. This way the browser can store the token in a cookie after one authenticated visit.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fastapi import Cookie, Response
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
|
# In main.py, add this route:
|
||||||
|
@app.get("/login")
|
||||||
|
async def login(token: str, response: Response):
|
||||||
|
if token != TOKEN:
|
||||||
|
raise HTTPException(status_code=401, detail="invalid token")
|
||||||
|
resp = RedirectResponse(url="/")
|
||||||
|
resp.set_cookie("eagle_token", token, httponly=True, samesite="strict", max_age=86400*30)
|
||||||
|
return resp
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `OriginAuthMiddleware.dispatch` to also check `request.cookies.get("eagle_token")`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Developer/eagle-dash
|
||||||
|
git add . && git commit -m "feat: cloudflared tunnel + cookie-based browser auth"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Phase 2 Outline: Network Security Panel
|
## Phase 2 Outline: Network Security Panel
|
||||||
|
|
||||||
*Not in this plan — build after Phase 1 ships.*
|
*Not in this plan — build after Phase 1 ships.*
|
||||||
@@ -1083,3 +1452,7 @@ Implementation: extend `registry.py` with a `bind_check` health type that calls
|
|||||||
- Some services (Ollama, MLX LM) bind to `0.0.0.0` and are accessible from LAN — displayed as `⚠️ lan` badge.
|
- Some services (Ollama, MLX LM) bind to `0.0.0.0` and are accessible from LAN — displayed as `⚠️ lan` badge.
|
||||||
- The heartbeat.sh log path uses `~` — expand it before opening: `Path(svc.log).expanduser()` — already handled in `main.py`.
|
- The heartbeat.sh log path uses `~` — expand it before opening: `Path(svc.log).expanduser()` — already handled in `main.py`.
|
||||||
- launchctl `stop` doesn't prevent KeepAlive services from restarting — `unload` is needed for a permanent stop (not exposed in UI on purpose, to prevent accidents).
|
- launchctl `stop` doesn't prevent KeepAlive services from restarting — `unload` is needed for a permanent stop (not exposed in UI on purpose, to prevent accidents).
|
||||||
|
- **Auth / cloudflared:** cloudflared sets `X-Forwarded-For` but `request.client.host` is still `127.0.0.1` (cloudflared runs locally). Detect external origin by checking `X-Forwarded-For` header presence, not just `client.host`.
|
||||||
|
- **FileBrowser iframe:** `FB_NOAUTH=true` env var name may differ by image version — check `docker run filebrowser/filebrowser --help` if the login screen still appears. Alternatively set `--username admin --password ""`.
|
||||||
|
- **Cookie auth and SameSite:** the cookie set by `/login` uses `samesite=strict`, which blocks it on cross-site redirects. Use `samesite=lax` instead if the login flow breaks in some browsers.
|
||||||
|
- **Pages tab iframe:** `sandbox="allow-same-origin allow-scripts"` will block pages that set `X-Frame-Options: DENY`. This is expected — most external sites won't load; local services usually will.
|
||||||
|
|||||||
Reference in New Issue
Block a user