added some quick .md files
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
# Auth — Service Registry & Persistence
|
||||
|
||||
The authentication system lets Discord users link their accounts to external
|
||||
services (currently **Jellyfin**) so the agent can perform actions on their
|
||||
behalf (e.g. checking watch history).
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
gateway/auth/ gateway/v1/auth.py
|
||||
┌──────────────────────┐ ┌──────────────────────────────┐
|
||||
│ AuthService (ABC) │ │ GET /api/v1/auth/login │
|
||||
│ ├─ JellyfinAuth │◀─────────│ POST /api/v1/auth/login │
|
||||
│ └─ (Plex, Seerr…) │ │ GET /api/v1/auth/status │
|
||||
│ │ │ GET /api/v1/auth/reset │
|
||||
└─────────┬────────────┘ └──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
src/auth_store.py
|
||||
┌──────────────────────┐
|
||||
│ SQLite │
|
||||
│ ├─ link_tokens │ one-time tokens sent via Discord DM
|
||||
│ └─ user_auth │ per-user, per-service credentials
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `gateway/auth/__init__.py` | Abstract `AuthService` base class + global registry |
|
||||
| `gateway/auth/jellyfin.py` | Jellyfin implementation — Quick Connect + username/password |
|
||||
| `gateway/v1/auth.py` | REST endpoints for the web-based login flow |
|
||||
| `src/auth_store.py` | SQLite persistence for link tokens and stored credentials |
|
||||
|
||||
---
|
||||
|
||||
## Flow: Discord User Links Jellyfin
|
||||
|
||||
```
|
||||
Discord DM Web Browser Jellyfin Server
|
||||
│ │ │
|
||||
│ 1. /login jellyfin │ │
|
||||
│ ──────────────────────────────▶│ │
|
||||
│ Bot creates link token in │ │
|
||||
│ SQLite, DMs the user a URL │ │
|
||||
│ │ │
|
||||
│ 2. User clicks link │ │
|
||||
│ ◀─────────────────────────────▶│ │
|
||||
│ │ GET /api/v1/auth/login │
|
||||
│ │ ?service=jellyfin │
|
||||
│ │ &token=xxx&discord_id=123 │
|
||||
│ │ │
|
||||
│ │ 3. Serve Quick Connect form │
|
||||
│ │ ◀──────────────────────────── │
|
||||
│ │ │
|
||||
│ │ 4. Initiate Quick Connect │
|
||||
│ │ ─────────────────────────────▶│
|
||||
│ │ POST /QuickConnect/Initiate │
|
||||
│ │ ◀── { Code: "ABC123" } │
|
||||
│ │ │
|
||||
│ 5. User enters code in │ │
|
||||
│ Jellyfin app │ │
|
||||
│ │ │
|
||||
│ │ 6. Poll: is it authorized? │
|
||||
│ │ ─────────────────────────────▶│
|
||||
│ │ GET /QuickConnect/Connect │
|
||||
│ │ ◀── Authenticated + Token │
|
||||
│ │ │
|
||||
│ 7. auth_store saves: │ │
|
||||
│ (discord_id, jellyfin, │ │
|
||||
│ AccessToken, username) │ │
|
||||
│ │ │
|
||||
│ 8. "✅ Linked to Jellyfin!" │ │
|
||||
│ ◀───────────────────────────── │ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AuthService Base Class
|
||||
|
||||
```python
|
||||
class AuthService(ABC):
|
||||
name: str # "jellyfin"
|
||||
display_name: str # "Jellyfin"
|
||||
|
||||
def render_login_form(token, discord_id) -> str: ...
|
||||
async def authenticate(form_data) -> AuthResult: ...
|
||||
```
|
||||
|
||||
Add a new service (e.g. Plex, Seerr) by subclassing `AuthService`, dropping
|
||||
the module in `gateway/auth/`, and calling `register_auth_service()` at import
|
||||
time. The REST endpoints and auth store work generically — no changes needed.
|
||||
|
||||
---
|
||||
|
||||
## Current Implementation: Jellyfin
|
||||
|
||||
`gateway/auth/jellyfin.py` supports two flows:
|
||||
|
||||
| Method | How it works |
|
||||
|---|---|
|
||||
| **Quick Connect** (primary) | Calls Jellyfin's `/QuickConnect/Initiate` → polls `/QuickConnect/Connect` → stores the `AccessToken` |
|
||||
| **Username/Password** (fallback) | Renders an HTML form → user submits credentials → calls `/Users/AuthenticateByName` → stores the `AccessToken` |
|
||||
|
||||
The stored credentials include:
|
||||
- `external_user_id` — Jellyfin user ID
|
||||
- `external_name` — Jellyfin username
|
||||
- `credentials` dict — `{"AccessToken": "...", "ServerURL": "..."}`
|
||||
|
||||
---
|
||||
|
||||
## Auth Store (SQLite)
|
||||
|
||||
Two tables in `data/auth.db`:
|
||||
|
||||
```sql
|
||||
-- One-time tokens for the web login flow (expire after 10 min)
|
||||
CREATE TABLE link_tokens (
|
||||
token TEXT PRIMARY KEY,
|
||||
discord_id INTEGER NOT NULL,
|
||||
service TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
used INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
-- Per-user, per-service stored credentials
|
||||
CREATE TABLE user_auth (
|
||||
discord_id INTEGER NOT NULL,
|
||||
service TEXT NOT NULL,
|
||||
external_user_id TEXT,
|
||||
external_name TEXT,
|
||||
credentials TEXT, -- JSON
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (discord_id, service)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Skill-Level Auth Gating
|
||||
|
||||
Skills can declare `requires_auth=["jellyfin"]`. When a tool is executed,
|
||||
the skill system checks the auth store. If the user isn't linked:
|
||||
|
||||
1. The tool returns `ToolResult.fail("Please login first using /login jellyfin")`
|
||||
2. The LLM relays this message to the user in Discord
|
||||
3. The user types `/login jellyfin` → Quick Connect flow → re-linked → try again
|
||||
Reference in New Issue
Block a user