small refactor of the structure
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Auth Service registry — generic, pluggable authentication for any service.
|
||||
|
||||
Add a new service (Plex, Seerr, etc.) by:
|
||||
1. Subclassing AuthService
|
||||
2. Dropping the module in this package
|
||||
3. Calling register_auth_service() at import time
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AuthResult — returned by AuthService.authenticate()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class AuthResult:
|
||||
"""Outcome of a credential validation attempt."""
|
||||
success: bool
|
||||
external_user_id: Optional[str] = None
|
||||
external_name: Optional[str] = None
|
||||
credentials: Optional[dict] = None
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AuthService — abstract base class
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class AuthService(ABC):
|
||||
"""A service that users can authenticate against (Jellyfin, Seerr, Plex, etc.)
|
||||
|
||||
Subclasses must implement:
|
||||
- name : unique identifier used in URLs and DB keys
|
||||
- display_name : human-readable label shown in Discord
|
||||
- render_login_form(token, discord_id) → HTML string
|
||||
- authenticate(form_data) → AuthResult
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""Unique service name: "jellyfin", "seerr", etc."""
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def display_name(self) -> str:
|
||||
"""Human-readable: "Jellyfin", "Seerr", "Plex" """
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def render_login_form(self, token: str, discord_id: int) -> str:
|
||||
"""Return HTML string with a login form for this service.
|
||||
|
||||
The form MUST include these hidden fields:
|
||||
<input type="hidden" name="token" value="{token}">
|
||||
<input type="hidden" name="discord_id" value="{discord_id}">
|
||||
<input type="hidden" name="service" value="{self.name}">
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def authenticate(self, form_data: dict) -> AuthResult:
|
||||
"""Validate credentials against the service. Return AuthResult."""
|
||||
...
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Global registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_registry: dict[str, AuthService] = {}
|
||||
|
||||
|
||||
def register_auth_service(svc: AuthService) -> None:
|
||||
"""Register an AuthService so it can be looked up by name."""
|
||||
_registry[svc.name] = svc
|
||||
|
||||
|
||||
def get_auth_service(name: str) -> AuthService | None:
|
||||
"""Look up a registered AuthService by name."""
|
||||
return _registry.get(name)
|
||||
|
||||
|
||||
def list_auth_services() -> list[str]:
|
||||
"""Return names of all registered auth services."""
|
||||
return list(_registry.keys())
|
||||
Reference in New Issue
Block a user