diff --git a/backend/src/appointment/controller/calendar.py b/backend/src/appointment/controller/calendar.py index fccf00618..5f428c951 100644 --- a/backend/src/appointment/controller/calendar.py +++ b/backend/src/appointment/controller/calendar.py @@ -33,6 +33,7 @@ from ..database.models import CalendarProvider, BookingStatus from ..database import schemas, models, repo from ..controller.mailer import Attachment +from ..exceptions.calendar import TestConnectionFailed from ..exceptions.validation import RemoteCalendarConnectionError from ..l10n import l10n from ..tasks.emails import send_invite_email, send_pending_email, send_rejection_email @@ -329,24 +330,36 @@ def test_connection(self) -> bool: except IndexError as ex: # Library has an issue with top level urls, probably due to caldav spec? logging.error(f'IE: Error testing connection {ex}') - return False + raise TestConnectionFailed(reason=None) except KeyError as ex: logging.error(f'KE: Error testing connection {ex}') - return False + raise TestConnectionFailed(reason=None) + except requests.exceptions.RequestException: + raise TestConnectionFailed(reason=None) + except NotImplementedError: + # Doesn't support authorization by digest, bearer, or basic header values + raise TestConnectionFailed(reason=l10n('remote-calendar-reason-doesnt-support-auth')) except ( - requests.exceptions.RequestException, caldav.lib.error.NotFoundError, caldav.lib.error.PropfindError, caldav.lib.error.AuthorizationError ) as ex: """ - RequestException: Max retries exceeded, bad connection, missing schema, etc... NotFoundError: Good server, bad url. PropfindError: Some properties could not be retrieved. AuthorizationError: Credentials are not accepted. """ logging.error(f'Test Connection Error: {ex}') - return False + + # Don't use the default "no reason" error message if we encounter it. + if ex.reason == caldav.lib.error.DAVError.reason: + ex.reason = None + if ex.reason == 'Unauthorized': + # ex.reason seems to be pulling from status codes for some errors? + # Let's replace this with our own. + ex.reason = l10n('remote-calendar-reason-unauthorized') + + raise TestConnectionFailed(reason=ex.reason) # They need at least VEVENT support for appointment to work. return supports_vevent diff --git a/backend/src/appointment/exceptions/calendar.py b/backend/src/appointment/exceptions/calendar.py index f5a1b9899..03e749f6d 100644 --- a/backend/src/appointment/exceptions/calendar.py +++ b/backend/src/appointment/exceptions/calendar.py @@ -1,8 +1,17 @@ class EventNotCreatedException(Exception): """Raise if an event cannot be created on a remote calendar""" + pass class EventNotDeletedException(Exception): """Raise if an event cannot be deleted on a remote calendar""" + pass + + +class TestConnectionFailed(Exception): + """Raise if test connection fails, include remote error message.""" + + def __init__(self, reason: str | None = None): + self.reason = reason diff --git a/backend/src/appointment/exceptions/validation.py b/backend/src/appointment/exceptions/validation.py index b01689ffa..394adefe4 100644 --- a/backend/src/appointment/exceptions/validation.py +++ b/backend/src/appointment/exceptions/validation.py @@ -205,9 +205,19 @@ def get_msg(self): class RemoteCalendarConnectionError(APIException): id_code = 'REMOTE_CALENDAR_CONNECTION_ERROR' status_code = 400 + reason = None + + def __init__(self, reason: str|None, **kwargs): + if reason: + self.reason = reason + super().__init__(**kwargs) def get_msg(self): - return l10n('remote-calendar-connection-error') + reason = self.reason + if not self.reason: + reason = l10n('unknown-error-short') + + return l10n('remote-calendar-connection-error', {'reason': reason}) class EventCouldNotBeAccepted(APIException): diff --git a/backend/src/appointment/l10n/de/main.ftl b/backend/src/appointment/l10n/de/main.ftl index 1b26cccdf..f97d10eac 100644 --- a/backend/src/appointment/l10n/de/main.ftl +++ b/backend/src/appointment/l10n/de/main.ftl @@ -19,6 +19,7 @@ health-bad = Zustand schlecht ## General Exceptions unknown-error = Ein unbekannter Fehler ist aufgetreten. Bitte später noch einmal versuchen. +unknown-error-short = Unbekannter Fehler appointment-not-found = Der Termin konnte nicht gefunden werden. calendar-not-found = Der Kalender konnte nicht gefunden werden. @@ -45,7 +46,15 @@ not-in-allow-list = Deine E-Mail-Adresse ist nicht in der Liste erlaubter Adress schedule-not-active = Der Zeitplan wurde abgeschaltet. Bitte für weitere Informationen den Eigentümer des Zeitplans kontaktieren. -remote-calendar-connection-error = Der angebundene Kalender konnte nicht erreicht werden. Bitte die Verbindungsinformationen überprüfen und noch einmal versuchen. +remote-calendar-connection-error = Der angebundene Kalender konnte nicht erreicht werden: {$reason}. +remote-calendar-connection-error = The remote calendar could not be reached due to {$reason}. + + Bitte die Verbindungsinformationen überprüfen und noch einmal versuchen. + +# Possible entries for $reason, +remote-calendar-reason-doesnt-support-caldav = Der Kalender bietet keine CalDAV-Unterstützung +remote-calendar-reason-doesnt-support-auth = Der Kalender unterstützt keine Authentifizierung +remote-calendar-reason-unauthorized = Es gibt ein Problem mit Benutzername oder Passwort event-could-not-be-accepted = Es ist ein Fehler bei der Annahme der Buchungsdaten aufgetreten. Bitte später noch einmal versuchen. event-could-not-be-deleted = Es ist ein Fehler beim Entfernen des vorläufigen Termins aufgetreten. Bitte später noch einmal versuchen. diff --git a/backend/src/appointment/l10n/en/main.ftl b/backend/src/appointment/l10n/en/main.ftl index 541fb8d6b..fb5b54c14 100644 --- a/backend/src/appointment/l10n/en/main.ftl +++ b/backend/src/appointment/l10n/en/main.ftl @@ -19,6 +19,7 @@ health-bad = Health BAD ## General Exceptions unknown-error = An unknown error occurred. Please try again later. +unknown-error-short = an unknown error appointment-not-found = The appointment could not be found. calendar-not-found = The calendar could not be found. @@ -45,7 +46,14 @@ not-in-allow-list = Your email is not in the allow list. schedule-not-active = The schedule has been turned off. Please contact the schedule owner for more information. -remote-calendar-connection-error = The remote calendar could not be reached. Please verify your connection information and try again. +remote-calendar-connection-error = The remote calendar could not be reached due to {$reason}. + + Please verify your connection information and try again. + +# Possible entries for $reason, +remote-calendar-reason-doesnt-support-caldav = the remote calendar does not support CalDAV +remote-calendar-reason-doesnt-support-auth = the remote calendar does not support authentication +remote-calendar-reason-unauthorized = an issue with your username or password event-could-not-be-accepted = There was an error accepting the booking details. Please try again later. event-could-not-be-deleted = There was an error removing the hold event. Please try again later. diff --git a/backend/src/appointment/routes/caldav.py b/backend/src/appointment/routes/caldav.py index 41e98088f..221b49891 100644 --- a/backend/src/appointment/routes/caldav.py +++ b/backend/src/appointment/routes/caldav.py @@ -12,8 +12,10 @@ from appointment.database import models, schemas, repo from appointment.dependencies.auth import get_subscriber from appointment.dependencies.database import get_db, get_redis +from appointment.exceptions.calendar import TestConnectionFailed from appointment.exceptions.misc import UnexpectedBehaviourWarning from appointment.exceptions.validation import RemoteCalendarConnectionError +from appointment.l10n import l10n router = APIRouter() @@ -99,8 +101,13 @@ def caldav_autodiscover_auth( calendar_id=None, ) - if not con.test_connection(): - raise RemoteCalendarConnectionError() + # If it returns False it doesn't support VEVENT (aka caldav) + # If it raises an exception there's a connection problem + try: + if not con.test_connection(): + raise RemoteCalendarConnectionError(reason=l10n('remote-calendar-reason-doesnt-support-caldav')) + except TestConnectionFailed as ex: + raise RemoteCalendarConnectionError(reason=ex.reason) caldav_id = json.dumps([connection.url, connection.user]) external_connection = repo.external_connection.get_by_type( diff --git a/frontend/src/tbpro/elements/NoticeBar.vue b/frontend/src/tbpro/elements/NoticeBar.vue index 99e5de0a0..b7ccd7932 100644 --- a/frontend/src/tbpro/elements/NoticeBar.vue +++ b/frontend/src/tbpro/elements/NoticeBar.vue @@ -51,6 +51,7 @@ const isError = props.type === 'error'; font-size: 0.8125rem; font-weight: 700; line-height: 1; + white-space: pre; } .notice { position: relative;