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