Skip to content

Commit

Permalink
Refactor exception names and definition flow. And replace a few more …
Browse files Browse the repository at this point in the history
…in schedule.py
  • Loading branch information
MelissaAutumn committed Dec 21, 2023
1 parent 1ad4879 commit 4bae60f
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 74 deletions.
8 changes: 4 additions & 4 deletions backend/src/appointment/dependencies/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from ..database import repo, schemas
from ..dependencies.database import get_db
from ..exceptions.validation import APIInvalidToken
from ..exceptions.validation import InvalidTokenException

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

Expand All @@ -19,9 +19,9 @@ def get_user_from_token(db, token: str):
payload = jwt.decode(token, os.getenv('JWT_SECRET'), algorithms=[os.getenv('JWT_ALGO')])
sub = payload.get("sub")
if sub is None:
raise HTTPException(401, "Could not validate credentials")
raise InvalidTokenException()
except JWTError:
raise HTTPException(401, "Could not validate credentials")
raise InvalidTokenException()

id = sub.replace('uid-', '')
return repo.get_subscriber(db, int(id))
Expand All @@ -35,6 +35,6 @@ def get_subscriber(
user = get_user_from_token(db, token)

if user is None:
raise APIInvalidToken()
raise InvalidTokenException()

return user
92 changes: 74 additions & 18 deletions backend/src/appointment/exceptions/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,93 @@
from ..l10n import l10n


class APIInvalidToken(HTTPException):
"""Raise when the subscriber could not be parsed from the auth token"""
class APIException(HTTPException):
"""Base exception for all custom API exceptions
Custom messages are defined in a function, because l10n needs context set before use."""
status_code = 500

def __init__(self, **kwargs):
super().__init__(status_code=401, detail=l10n('protected-route-fail'), **kwargs)
super().__init__(status_code=self.status_code, detail=self.get_msg(), **kwargs)

def get_msg(self):
return l10n('unknown-error')


class InvalidTokenException(APIException):
"""Raise when the subscriber could not be parsed from the auth token"""
status_code = 401

def get_msg(self):
return l10n('protected-route-fail')

class APISubscriberNotFound(HTTPException):

class SubscriberNotFoundException(APIException):
"""Raise when the calendar is not found during route validation"""
def __init__(self, **kwargs):
super().__init__(status_code=404, detail=l10n('calendar-not-found'), **kwargs)
status_code = 404

def get_msg(self):
return l10n('subscriber-not-found')

class APICalendarNotFound(HTTPException):

class CalendarNotFoundException(APIException):
"""Raise when the calendar is not found during route validation"""
def __init__(self, **kwargs):
super().__init__(status_code=404, detail=l10n('calendar-not-found'), **kwargs)
status_code = 404

def get_msg(self):
return l10n('calendar-not-found')

class APICalendarNotAuthorized(HTTPException):

class CalendarNotAuthorizedException(APIException):
"""Raise when the calendar is owned by someone else during route validation"""
def __init__(self, **kwargs):
super().__init__(status_code=403, detail=l10n('calendar-not-auth'), **kwargs)
status_code = 403

def get_msg(self):
return l10n('calendar-not-auth')


class CalendarNotConnectedException(APIException):
"""Raise when the calendar is owned by someone else during route validation"""
status_code = 403

class APIAppointmentNotFound(HTTPException):
def get_msg(self):
return l10n('calendar-not-active')


class AppointmentNotFoundException(APIException):
"""Raise when the appointment is not found during route validation"""
def __init__(self, **kwargs):
super().__init__(status_code=404, detail=l10n('appointment-not-found'), **kwargs)
status_code = 404

def get_msg(self):
return l10n('appointment-not-found')


class APIAppointmentNotAuthorized(HTTPException):
class AppointmentNotAuthorizedException(APIException):
"""Raise when the appointment is owned by someone else during route validation"""
def __init__(self, **kwargs):
super().__init__(status_code=403, detail=l10n('appointment-not-auth'), **kwargs)
status_code = 403

def get_msg(self):
return l10n('appointment-not-auth')


class ScheduleNotFoundException(APIException):
"""Raise when the schedule is not found during route validation"""
status_code = 404

def get_msg(self):
return l10n('schedule-not-found')


class ScheduleNotAuthorizedException(APIException):
"""Raise when the schedule is owned by someone else during route validation"""
status_code = 403

def get_msg(self):
return l10n('schedule-not-auth')


class ZoomNotConnectedException(APIException):
"""Raise if the user requires a zoom connection during route validation"""
status_code = 400

def get_msg(self):
return l10n('zoom-not-connected')
20 changes: 13 additions & 7 deletions backend/src/appointment/l10n/en/main.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,27 @@ health-ok = System is operational
## General Exceptions

unknown-error = An unknown error occurred. Please try again later.
appointment-not-found = The appointment could not be found.
calendar-not-found = The calendar could not be found.
schedule-not-found = The schedule could not be found.
slot-not-found = The time slot you have selected could not be found. Please try again.
subscriber-not-found = The subscriber could not be found.
appointment-not-auth = You are not authorized to view or modify this appointment.
calendar-not-auth = You are not authorized to view or modify this calendar.
schedule-not-auth = You are not authorized to view or modify this schedule.
slot-not-auth = You are not authorized to view or modify this time slot.
account-delete-fail = There was a problem deleting your data. This incident has been logged and your data will manually be removed.
protected-route-fail = No valid authentication credentials provided.
username-not-available = This username has already been taken.
invalid-link = This link is no longer valid.
subscriber-not-found = The subscriber could not be found.
calendar-not-found = The calendar could not be found.
calendar-not-auth = You are not authorized to view or modify this calendar.
calendar-sync-fail = An error occurred while syncing calendars. Please try again later.
calendar-not-active = The calendar connection is not active.
appointment-not-found = The appointment could not be found.
appointment-not-auth = You are not authorized to view or modify this appointment.
slot-not-found = The time slot you have selected could not be found. Please try again.
slot-already-taken = The time slot you have selected is no longer available. Please try again.
slot-invalid-email = The email you have provided was not valid. Please try again.
slot-not-auth = You are not authorized to view or modify this time slot.
## Authentication Exceptions

Expand Down
52 changes: 26 additions & 26 deletions backend/src/appointment/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
from ..dependencies.auth import get_subscriber
from ..dependencies.database import get_db
from ..dependencies.zoom import get_zoom_client
from ..exceptions.validation import APICalendarNotFound, APICalendarNotAuthorized, APIAppointmentNotFound, \
APIAppointmentNotAuthorized, APISubscriberNotFound
from ..exceptions.validation import CalendarNotFoundException, CalendarNotAuthorizedException, AppointmentNotFoundException, \
AppointmentNotAuthorizedException, SubscriberNotFoundException, ZoomNotConnectedException, CalendarNotConnectedException
from ..l10n import l10n

router = APIRouter()
Expand Down Expand Up @@ -124,9 +124,9 @@ def read_my_calendar(id: int, db: Session = Depends(get_db), subscriber: Subscri
cal = repo.get_calendar(db, calendar_id=id)

if cal is None:
raise APICalendarNotFound()
raise CalendarNotFoundException()
if not repo.calendar_is_owned(db, calendar_id=id, subscriber_id=subscriber.id):
raise APICalendarNotAuthorized()
raise CalendarNotAuthorizedException()

return schemas.CalendarConnectionOut(
id=cal.id,
Expand All @@ -148,9 +148,9 @@ def update_my_calendar(
):
"""endpoint to update an existing calendar connection for authenticated subscriber"""
if not repo.calendar_exists(db, calendar_id=id):
raise APICalendarNotFound()
raise CalendarNotFoundException()
if not repo.calendar_is_owned(db, calendar_id=id, subscriber_id=subscriber.id):
raise APICalendarNotAuthorized()
raise CalendarNotAuthorizedException()

cal = repo.update_subscriber_calendar(db=db, calendar=calendar, calendar_id=id)
return schemas.CalendarOut(id=cal.id, title=cal.title, color=cal.color, connected=cal.connected)
Expand All @@ -164,9 +164,9 @@ def connect_my_calendar(
):
"""endpoint to update an existing calendar connection for authenticated subscriber"""
if not repo.calendar_exists(db, calendar_id=id):
raise APICalendarNotFound()
raise CalendarNotFoundException()
if not repo.calendar_is_owned(db, calendar_id=id, subscriber_id=subscriber.id):
raise APICalendarNotAuthorized()
raise CalendarNotAuthorizedException()

try:
cal = repo.update_subscriber_calendar_connection(db=db, calendar_id=id, is_connected=True)
Expand All @@ -179,9 +179,9 @@ def connect_my_calendar(
def delete_my_calendar(id: int, db: Session = Depends(get_db), subscriber: Subscriber = Depends(get_subscriber)):
"""endpoint to remove a calendar from db"""
if not repo.calendar_exists(db, calendar_id=id):
raise APICalendarNotFound()
raise CalendarNotFoundException()
if not repo.calendar_is_owned(db, calendar_id=id, subscriber_id=subscriber.id):
raise APICalendarNotAuthorized()
raise CalendarNotAuthorizedException()

cal = repo.delete_subscriber_calendar(db=db, calendar_id=id)
return schemas.CalendarOut(id=cal.id, title=cal.title, color=cal.color, connected=cal.connected)
Expand Down Expand Up @@ -246,7 +246,7 @@ def read_remote_events(
db_calendar = repo.get_calendar(db, calendar_id=id)

if db_calendar is None:
raise APICalendarNotFound()
raise CalendarNotFoundException()

if db_calendar.provider == CalendarProvider.google:
con = GoogleConnector(
Expand All @@ -271,13 +271,13 @@ def create_my_calendar_appointment(
):
"""endpoint to add a new appointment with slots for a given calendar"""
if not repo.calendar_exists(db, calendar_id=a_s.appointment.calendar_id):
raise APICalendarNotFound()
raise CalendarNotFoundException()
if not repo.calendar_is_owned(db, calendar_id=a_s.appointment.calendar_id, subscriber_id=subscriber.id):
raise APICalendarNotAuthorized()
raise CalendarNotAuthorizedException()
if not repo.calendar_is_connected(db, calendar_id=a_s.appointment.calendar_id):
raise HTTPException(status_code=403, detail=l10n('calendar-not-active'))
raise CalendarNotConnectedException()
if a_s.appointment.meeting_link_provider == MeetingLinkProviderType.zoom and subscriber.get_external_connection(ExternalConnectionType.zoom) is None:
raise HTTPException(status_code=400, detail=l10n('zoom-not-connected'))
raise ZoomNotConnectedException()
return repo.create_calendar_appointment(db=db, appointment=a_s.appointment, slots=a_s.slots)


Expand All @@ -287,9 +287,9 @@ def read_my_appointment(id: str, db: Session = Depends(get_db), subscriber: Subs
db_appointment = repo.get_appointment(db, appointment_id=id)

if db_appointment is None:
raise APIAppointmentNotFound()
raise AppointmentNotFoundException()
if not repo.appointment_is_owned(db, appointment_id=id, subscriber_id=subscriber.id):
raise APIAppointmentNotAuthorized()
raise AppointmentNotAuthorizedException()

return db_appointment

Expand All @@ -305,9 +305,9 @@ def update_my_appointment(
db_appointment = repo.get_appointment(db, appointment_id=id)

if db_appointment is None:
raise APIAppointmentNotFound()
raise AppointmentNotFoundException()
if not repo.appointment_is_owned(db, appointment_id=id, subscriber_id=subscriber.id):
raise APIAppointmentNotAuthorized()
raise AppointmentNotAuthorizedException()

return repo.update_calendar_appointment(db=db, appointment=a_s.appointment, slots=a_s.slots, appointment_id=id)

Expand All @@ -318,9 +318,9 @@ def delete_my_appointment(id: int, db: Session = Depends(get_db), subscriber: Su
db_appointment = repo.get_appointment(db, appointment_id=id)

if db_appointment is None:
raise APIAppointmentNotFound()
raise AppointmentNotFoundException()
if not repo.appointment_is_owned(db, appointment_id=id, subscriber_id=subscriber.id):
raise APIAppointmentNotAuthorized()
raise AppointmentNotAuthorizedException()

return repo.delete_calendar_appointment(db=db, appointment_id=id)

Expand All @@ -330,10 +330,10 @@ def read_public_appointment(slug: str, db: Session = Depends(get_db)):
"""endpoint to retrieve an appointment from db via public link and only expose necessary data"""
a = repo.get_public_appointment(db, slug=slug)
if a is None:
raise APIAppointmentNotFound()
raise AppointmentNotFoundException()
s = repo.get_subscriber_by_appointment(db=db, appointment_id=a.id)
if s is None:
raise APISubscriberNotFound()
raise SubscriberNotFoundException()
slots = [
schemas.SlotOut(id=sl.id, start=sl.start, duration=sl.duration, attendee_id=sl.attendee_id) for sl in a.slots
]
Expand All @@ -352,10 +352,10 @@ def update_public_appointment_slot(
"""endpoint to update a time slot for an appointment via public link and create an event in remote calendar"""
db_appointment = repo.get_public_appointment(db, slug=slug)
if db_appointment is None:
raise APIAppointmentNotFound()
raise AppointmentNotFoundException()
db_calendar = repo.get_calendar(db, calendar_id=db_appointment.calendar_id)
if db_calendar is None:
raise APICalendarNotFound()
raise CalendarNotFoundException()
if not repo.appointment_has_slot(db, appointment_id=db_appointment.id, slot_id=s_a.slot_id):
raise HTTPException(status_code=404, detail=l10n('slot-not-found'))
if not repo.slot_is_available(db, slot_id=s_a.slot_id):
Expand Down Expand Up @@ -441,7 +441,7 @@ def public_appointment_serve_ics(slug: str, slot_id: int, db: Session = Depends(
"""endpoint to serve ICS file for time slot to download"""
db_appointment = repo.get_public_appointment(db, slug=slug)
if db_appointment is None:
raise APIAppointmentNotFound()
raise AppointmentNotFoundException()
if not repo.appointment_has_slot(db, appointment_id=db_appointment.id, slot_id=slot_id):
raise HTTPException(status_code=404, detail=l10n('slot-not-auth'))
slot = repo.get_slot(db=db, slot_id=slot_id)
Expand Down
Loading

0 comments on commit 4bae60f

Please sign in to comment.