Initial curated Unraid-AppOps-Gateway source from 3bd6f6fee310125bfbb4d4650389a817f498724d
This commit is contained in:
commit
1dfbca08a1
528 files changed
+131054
No files matched your search
@@ -0,0 +1,233 @@
|
||||
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))
|
||||
Reference in new issue
Block a user