Files
Unraid-AppOps-Gateway-Public/backend/app/api/routes_setup.py
T

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))