234 lines
9.6 KiB
Python
234 lines
9.6 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
from sqlmodel import Session
|
|
|
|
from ..connectors.deluge import DelugeConnector
|
|
from ..connectors.nginx_proxy_manager import NginxProxyManagerConnector
|
|
from ..connectors.authentik import AuthentikConnector
|
|
from ..connectors.plex import PlexConnector
|
|
from ..connectors.prowlarr import ProwlarrConnector
|
|
from ..connectors.radarr import RadarrConnector
|
|
from ..connectors.sonarr import SonarrConnector
|
|
from ..connectors.tautulli import TautulliConnector
|
|
from ..connectors.widefrog import WidefrogConnector
|
|
from ..core.redaction import redact_message
|
|
from ..db.session import get_session
|
|
from ..services.setup_service import (
|
|
complete_setup,
|
|
get_setup_status,
|
|
list_connector_settings,
|
|
mark_setup_complete,
|
|
mark_voice_configured,
|
|
reconcile_setup,
|
|
save_connector_settings,
|
|
serialize_connector_settings,
|
|
test_portainer_setup,
|
|
test_unraid_setup,
|
|
)
|
|
from ..services.secret_store import list_secret_status, secret_storage_status
|
|
from ..core.network_security import UnsafeConnectorUrl
|
|
|
|
router = APIRouter(prefix='/api')
|
|
|
|
|
|
class ConnectorSettingsRequest(BaseModel):
|
|
model_config = ConfigDict(extra='forbid')
|
|
url: str | None = Field(default=None, max_length=2048)
|
|
endpointId: str | None = Field(default=None, max_length=80)
|
|
verifyTls: bool | None = None
|
|
apiKey: str | None = Field(default=None, max_length=8192)
|
|
token: str | None = Field(default=None, max_length=8192)
|
|
password: str | None = Field(default=None, max_length=8192)
|
|
secret: str | None = Field(default=None, max_length=8192)
|
|
identity: str | None = Field(default=None, max_length=320)
|
|
outpostUrl: str | None = Field(default=None, max_length=2048)
|
|
expectEmbeddedOutpost: bool | None = None
|
|
|
|
|
|
def _connector_payload(payload: ConnectorSettingsRequest) -> dict:
|
|
return payload.model_dump(exclude_none=True)
|
|
|
|
|
|
def _save_connector_settings(session: Session, connector_id: str, payload: dict):
|
|
try:
|
|
return save_connector_settings(session, connector_id, payload)
|
|
except UnsafeConnectorUrl as exc:
|
|
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
|
|
|
|
|
def _authentik_setup_result(result: dict, settings: dict) -> dict:
|
|
status = str(result.get('status', 'error'))
|
|
message = str(result.get('reason') or status)
|
|
required_fields: list[str] = []
|
|
if status == 'unreachable' and 'HTTP 404' in message:
|
|
status = 'url_error'
|
|
required_fields = ['AUTHENTIK_URL']
|
|
message = 'Authentik API gaf HTTP 404 terug. Controleer dat de Authentik URL de root/base URL is, niet een app-, flow- of outpost-pad.'
|
|
if status == 'auth_failed':
|
|
required_fields = ['AUTHENTIK_TOKEN']
|
|
message = 'Authentik heeft de token geweigerd: token ongeldig of verlopen. Gebruik de waarde van een Authentik API Token, niet de tokennaam, identifier of een App password, en sla die opnieuw op.'
|
|
return {
|
|
'success': status == 'connected',
|
|
'status': status,
|
|
'connected': status == 'connected',
|
|
'message': message,
|
|
'requiredFields': required_fields,
|
|
'settings': settings,
|
|
}
|
|
|
|
|
|
def _normalized_url(settings: dict) -> str:
|
|
safe_config = settings.get('safeConfig') if isinstance(settings.get('safeConfig'), dict) else {}
|
|
return str(safe_config.get('url') or '').strip().rstrip('/').lower()
|
|
|
|
|
|
def _connector_setup_result(connector_id: str, result: dict, settings: dict, session: Session) -> dict:
|
|
status = str(result.get('status', 'error'))
|
|
message = str(result.get('reason') or status)
|
|
required_fields = list(settings.get('missing') or [])
|
|
if not required_fields and status in {'error', 'unreachable', 'auth_failed'}:
|
|
required_fields = list(_CONNECTOR_REQUIRED_FIELDS.get(connector_id, []))
|
|
|
|
if connector_id == 'prowlarr' and status in {'error', 'unreachable', 'auth_failed'} and message == 'unauthorized':
|
|
prowlarr_url = _normalized_url(settings)
|
|
for sibling_id in ('radarr', 'sonarr'):
|
|
sibling = serialize_connector_settings(session, sibling_id)
|
|
if prowlarr_url and prowlarr_url == _normalized_url(sibling):
|
|
label = 'Radarr' if sibling_id == 'radarr' else 'Sonarr'
|
|
return {
|
|
'success': False,
|
|
'status': 'url_mismatch',
|
|
'connected': False,
|
|
'message': f'Prowlarr geeft unauthorized terug en gebruikt dezelfde URL als {label}. Controleer PROWLARR_URL en PROWLARR_API_KEY; Prowlarr draait meestal op poort 9696.',
|
|
'requiredFields': ['PROWLARR_URL', 'PROWLARR_API_KEY'],
|
|
'settings': settings,
|
|
}
|
|
|
|
return {
|
|
'success': status == 'connected',
|
|
'status': status,
|
|
'connected': status == 'connected',
|
|
'message': redact_message(message),
|
|
'requiredFields': required_fields,
|
|
'settings': settings,
|
|
}
|
|
|
|
|
|
_CONNECTOR_TESTS = {
|
|
'sonarr': SonarrConnector,
|
|
'radarr': RadarrConnector,
|
|
'prowlarr': ProwlarrConnector,
|
|
'deluge': DelugeConnector,
|
|
'plex': PlexConnector,
|
|
'tautulli': TautulliConnector,
|
|
'widefrog': WidefrogConnector,
|
|
}
|
|
|
|
|
|
_CONNECTOR_REQUIRED_FIELDS = {
|
|
'sonarr': ['SONARR_URL', 'SONARR_API_KEY'],
|
|
'radarr': ['RADARR_URL', 'RADARR_API_KEY'],
|
|
'prowlarr': ['PROWLARR_URL', 'PROWLARR_API_KEY'],
|
|
'deluge': ['DELUGE_URL', 'DELUGE_PASSWORD'],
|
|
'plex': ['PLEX_URL', 'PLEX_TOKEN'],
|
|
'tautulli': ['TAUTULLI_URL', 'TAUTULLI_API_KEY'],
|
|
'widefrog': ['WIDEFROG_URL'],
|
|
}
|
|
|
|
|
|
@router.get('/setup/status')
|
|
def setup_status(session: Session = Depends(get_session)):
|
|
return get_setup_status(session)
|
|
|
|
|
|
@router.post('/setup/complete')
|
|
def setup_complete(session: Session = Depends(get_session)):
|
|
return complete_setup(session)
|
|
|
|
|
|
@router.post('/setup/reconcile')
|
|
def setup_reconcile(session: Session = Depends(get_session)):
|
|
return reconcile_setup(session)
|
|
|
|
|
|
@router.post('/setup/mark-complete')
|
|
def setup_mark_complete(payload: dict, session: Session = Depends(get_session)):
|
|
return mark_setup_complete(session, payload)
|
|
|
|
|
|
@router.post('/setup/voice/mark-configured')
|
|
def setup_voice_configured(session: Session = Depends(get_session)):
|
|
return mark_voice_configured(session)
|
|
|
|
|
|
@router.post('/setup/test/portainer')
|
|
def setup_test_portainer(session: Session = Depends(get_session)):
|
|
return test_portainer_setup(session)
|
|
|
|
|
|
@router.post('/setup/test/unraid')
|
|
def setup_test_unraid(session: Session = Depends(get_session)):
|
|
return test_unraid_setup(session)
|
|
|
|
|
|
@router.post('/setup/test/connector/{connector_id}')
|
|
def setup_test_connector(connector_id: str, session: Session = Depends(get_session)):
|
|
if connector_id == 'portainer':
|
|
return test_portainer_setup(session)
|
|
if connector_id == 'unraid':
|
|
return test_unraid_setup(session)
|
|
if connector_id == 'nginx_proxy_manager':
|
|
settings = serialize_connector_settings(session, connector_id)
|
|
if settings['missing']:
|
|
return {'success': False, 'status': 'missing_config', 'connected': False, 'requiredFields': settings['missing'], 'message': 'missing_config', 'settings': settings}
|
|
result = NginxProxyManagerConnector().test_connection()
|
|
status = str(result.get('status', 'error'))
|
|
return {'success': status == 'connected', 'status': status, 'connected': status == 'connected', 'message': str(result.get('reason') or status), 'settings': settings}
|
|
if connector_id == 'authentik':
|
|
settings = serialize_connector_settings(session, connector_id)
|
|
if settings['missing']:
|
|
return {'success': False, 'status': 'missing_config', 'connected': False, 'requiredFields': settings['missing'], 'message': 'missing_config', 'settings': settings}
|
|
result = AuthentikConnector().test_connection()
|
|
return _authentik_setup_result(result, settings)
|
|
connector_cls = _CONNECTOR_TESTS.get(connector_id)
|
|
if connector_cls:
|
|
settings = serialize_connector_settings(session, connector_id)
|
|
if settings['missing']:
|
|
return {'success': False, 'status': 'missing_config', 'connected': False, 'requiredFields': settings['missing'], 'message': 'missing_config', 'settings': settings}
|
|
result = connector_cls().test_connection()
|
|
return _connector_setup_result(connector_id, result, settings, session)
|
|
return {
|
|
'success': False,
|
|
'status': 'unsupported',
|
|
'connected': False,
|
|
'message': f'setup_test_not_implemented_for_{connector_id}',
|
|
'settings': serialize_connector_settings(session, connector_id),
|
|
}
|
|
|
|
|
|
@router.get('/settings/connectors')
|
|
def settings_connectors(session: Session = Depends(get_session)):
|
|
return list_connector_settings(session)
|
|
|
|
|
|
@router.get('/settings/secrets')
|
|
def settings_secrets(session: Session = Depends(get_session)):
|
|
return {'storage': secret_storage_status(), 'secrets': list_secret_status(session)}
|
|
|
|
|
|
@router.patch('/settings/connectors/portainer')
|
|
def update_portainer_settings(payload: ConnectorSettingsRequest, session: Session = Depends(get_session)):
|
|
return _save_connector_settings(session, 'portainer', _connector_payload(payload))
|
|
|
|
|
|
@router.patch('/settings/connectors/unraid')
|
|
def update_unraid_settings(payload: ConnectorSettingsRequest, session: Session = Depends(get_session)):
|
|
return _save_connector_settings(session, 'unraid', _connector_payload(payload))
|
|
|
|
|
|
@router.patch('/settings/connectors/{connector_id}')
|
|
def update_connector_settings(connector_id: str, payload: ConnectorSettingsRequest, session: Session = Depends(get_session)):
|
|
return _save_connector_settings(session, connector_id, _connector_payload(payload))
|