From 08a62670c84fa24c366ca80810805f1a278dbceb Mon Sep 17 00:00:00 2001 From: Melissa Autumn Date: Thu, 21 Dec 2023 10:29:19 -0800 Subject: [PATCH] Add a few more exceptions and finish off schedule.py --- .../src/appointment/exceptions/validation.py | 32 +++++++++ backend/src/appointment/l10n/en/main.ftl | 1 + backend/src/appointment/routes/api.py | 64 ++++++++--------- backend/src/appointment/routes/schedule.py | 69 +++++++++++-------- 4 files changed, 105 insertions(+), 61 deletions(-) diff --git a/backend/src/appointment/exceptions/validation.py b/backend/src/appointment/exceptions/validation.py index e77b6732a..f4f244e00 100644 --- a/backend/src/appointment/exceptions/validation.py +++ b/backend/src/appointment/exceptions/validation.py @@ -23,6 +23,14 @@ def get_msg(self): return l10n('protected-route-fail') +class InvalidLinkException(APIException): + """Raise when verify_subscriber_link fails""" + status_code = 400 + + def get_msg(self): + return l10n('invalid-link') + + class SubscriberNotFoundException(APIException): """Raise when the calendar is not found during route validation""" status_code = 404 @@ -87,6 +95,30 @@ def get_msg(self): return l10n('schedule-not-auth') +class SlotNotFoundException(APIException): + """Raise when a timeslot is not found during route validation""" + status_code = 404 + + def get_msg(self): + return l10n('slot-not-found') + + +class SlotAlreadyTakenException(APIException): + """Raise when a timeslot is already taken during route validation""" + status_code = 403 + + def get_msg(self): + return l10n('slot-already-taken') + + +class SlotNotAuthorizedException(APIException): + """Raise when a slot is owned by someone else during route validation""" + status_code = 403 + + def get_msg(self): + return l10n('slot-not-auth') + + class ZoomNotConnectedException(APIException): """Raise if the user requires a zoom connection during route validation""" status_code = 400 diff --git a/backend/src/appointment/l10n/en/main.ftl b/backend/src/appointment/l10n/en/main.ftl index 2a0cac33a..fe61db3a9 100644 --- a/backend/src/appointment/l10n/en/main.ftl +++ b/backend/src/appointment/l10n/en/main.ftl @@ -29,6 +29,7 @@ username-not-available = This username has already been taken. invalid-link = This link is no longer valid. calendar-sync-fail = An error occurred while syncing calendars. Please try again later. calendar-not-active = The calendar connection is not active. +slot-not-found = There are no available time slots to book. 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. diff --git a/backend/src/appointment/routes/api.py b/backend/src/appointment/routes/api.py index 50c649833..3d9fa6586 100644 --- a/backend/src/appointment/routes/api.py +++ b/backend/src/appointment/routes/api.py @@ -24,8 +24,7 @@ from ..dependencies.auth import get_subscriber from ..dependencies.database import get_db from ..dependencies.zoom import get_zoom_client -from ..exceptions.validation import CalendarNotFoundException, CalendarNotAuthorizedException, AppointmentNotFoundException, \ - AppointmentNotAuthorizedException, SubscriberNotFoundException, ZoomNotConnectedException, CalendarNotConnectedException +from ..exceptions import validation from ..l10n import l10n router = APIRouter() @@ -100,7 +99,7 @@ def verify_signature(url: str = Body(..., embed=True), db: Session = Depends(get if repo.verify_subscriber_link(db, url): return True - raise HTTPException(400, l10n('invalid-link')) + raise validation.InvalidLinkException() @router.post("/cal", response_model=schemas.CalendarOut) @@ -124,9 +123,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 CalendarNotFoundException() + raise validation.CalendarNotFoundException() if not repo.calendar_is_owned(db, calendar_id=id, subscriber_id=subscriber.id): - raise CalendarNotAuthorizedException() + raise validation.CalendarNotAuthorizedException() return schemas.CalendarConnectionOut( id=cal.id, @@ -148,9 +147,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 CalendarNotFoundException() + raise validation.CalendarNotFoundException() if not repo.calendar_is_owned(db, calendar_id=id, subscriber_id=subscriber.id): - raise CalendarNotAuthorizedException() + raise validation.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) @@ -164,9 +163,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 CalendarNotFoundException() + raise validation.CalendarNotFoundException() if not repo.calendar_is_owned(db, calendar_id=id, subscriber_id=subscriber.id): - raise CalendarNotAuthorizedException() + raise validation.CalendarNotAuthorizedException() try: cal = repo.update_subscriber_calendar_connection(db=db, calendar_id=id, is_connected=True) @@ -179,9 +178,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 CalendarNotFoundException() + raise validation.CalendarNotFoundException() if not repo.calendar_is_owned(db, calendar_id=id, subscriber_id=subscriber.id): - raise CalendarNotAuthorizedException() + raise validation.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) @@ -246,7 +245,7 @@ def read_remote_events( db_calendar = repo.get_calendar(db, calendar_id=id) if db_calendar is None: - raise CalendarNotFoundException() + raise validation.CalendarNotFoundException() if db_calendar.provider == CalendarProvider.google: con = GoogleConnector( @@ -271,13 +270,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 CalendarNotFoundException() + raise validation.CalendarNotFoundException() if not repo.calendar_is_owned(db, calendar_id=a_s.appointment.calendar_id, subscriber_id=subscriber.id): - raise CalendarNotAuthorizedException() + raise validation.CalendarNotAuthorizedException() if not repo.calendar_is_connected(db, calendar_id=a_s.appointment.calendar_id): - raise CalendarNotConnectedException() + raise validation.CalendarNotConnectedException() if a_s.appointment.meeting_link_provider == MeetingLinkProviderType.zoom and subscriber.get_external_connection(ExternalConnectionType.zoom) is None: - raise ZoomNotConnectedException() + raise validation.ZoomNotConnectedException() return repo.create_calendar_appointment(db=db, appointment=a_s.appointment, slots=a_s.slots) @@ -287,9 +286,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 AppointmentNotFoundException() + raise validation.AppointmentNotFoundException() if not repo.appointment_is_owned(db, appointment_id=id, subscriber_id=subscriber.id): - raise AppointmentNotAuthorizedException() + raise validation.AppointmentNotAuthorizedException() return db_appointment @@ -305,9 +304,9 @@ def update_my_appointment( db_appointment = repo.get_appointment(db, appointment_id=id) if db_appointment is None: - raise AppointmentNotFoundException() + raise validation.AppointmentNotFoundException() if not repo.appointment_is_owned(db, appointment_id=id, subscriber_id=subscriber.id): - raise AppointmentNotAuthorizedException() + raise validation.AppointmentNotAuthorizedException() return repo.update_calendar_appointment(db=db, appointment=a_s.appointment, slots=a_s.slots, appointment_id=id) @@ -318,9 +317,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 AppointmentNotFoundException() + raise validation.AppointmentNotFoundException() if not repo.appointment_is_owned(db, appointment_id=id, subscriber_id=subscriber.id): - raise AppointmentNotAuthorizedException() + raise validation.AppointmentNotAuthorizedException() return repo.delete_calendar_appointment(db=db, appointment_id=id) @@ -330,10 +329,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 AppointmentNotFoundException() + raise validation.AppointmentNotFoundException() s = repo.get_subscriber_by_appointment(db=db, appointment_id=a.id) if s is None: - raise SubscriberNotFoundException() + raise validation.SubscriberNotFoundException() slots = [ schemas.SlotOut(id=sl.id, start=sl.start, duration=sl.duration, attendee_id=sl.attendee_id) for sl in a.slots ] @@ -352,14 +351,14 @@ 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 AppointmentNotFoundException() + raise validation.AppointmentNotFoundException() db_calendar = repo.get_calendar(db, calendar_id=db_appointment.calendar_id) if db_calendar is None: - raise CalendarNotFoundException() + raise validation.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')) + raise validation.SlotNotFoundException() if not repo.slot_is_available(db, slot_id=s_a.slot_id): - raise HTTPException(status_code=403, detail=l10n('slot-already-taken')) + raise validation.SlotAlreadyTakenException() if not validators.email(s_a.attendee.email): raise HTTPException(status_code=400, detail=l10n('slot-invalid-email')) @@ -441,12 +440,15 @@ 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 AppointmentNotFoundException() + raise validation.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')) + raise validation.SlotNotFoundException() + slot = repo.get_slot(db=db, slot_id=slot_id) if slot is None: - raise HTTPException(status_code=404, detail=l10n('slot-not-found')) + raise validation.SlotNotFoundException() + organizer = repo.get_subscriber_by_appointment(db=db, appointment_id=db_appointment.id) return schemas.FileDownload( diff --git a/backend/src/appointment/routes/schedule.py b/backend/src/appointment/routes/schedule.py index 59f26a6f0..1fe583b99 100644 --- a/backend/src/appointment/routes/schedule.py +++ b/backend/src/appointment/routes/schedule.py @@ -20,9 +20,7 @@ from zoneinfo import ZoneInfo from ..dependencies.zoom import get_zoom_client -from ..exceptions.validation import CalendarNotFoundException, CalendarNotAuthorizedException, ScheduleNotFoundException, \ - ScheduleNotAuthorizedException, ZoomNotConnectedException, CalendarNotConnectedException -from ..l10n import l10n +from ..exceptions import validation router = APIRouter() @@ -35,11 +33,11 @@ def create_calendar_schedule( ): """endpoint to add a new schedule for a given calendar""" if not repo.calendar_exists(db, calendar_id=schedule.calendar_id): - raise CalendarNotFoundException() + raise validation.CalendarNotFoundException() if not repo.calendar_is_owned(db, calendar_id=schedule.calendar_id, subscriber_id=subscriber.id): - raise CalendarNotAuthorizedException() + raise validation.CalendarNotAuthorizedException() if not repo.calendar_is_connected(db, calendar_id=schedule.calendar_id): - raise CalendarNotConnectedException() + raise validation.CalendarNotConnectedException() return repo.create_calendar_schedule(db=db, schedule=schedule) @@ -58,9 +56,9 @@ def read_schedule( """Gets information regarding a specific schedule""" schedule = repo.get_schedule(db, schedule_id=id) if schedule is None: - raise ScheduleNotFoundException() + raise validation.ScheduleNotFoundException() if not repo.schedule_is_owned(db, schedule_id=id, subscriber_id=subscriber.id): - raise ScheduleNotAuthorizedException() + raise validation.ScheduleNotAuthorizedException() return schedule @@ -73,11 +71,11 @@ def update_schedule( ): """endpoint to update an existing calendar connection for authenticated subscriber""" if not repo.schedule_exists(db, schedule_id=id): - raise ScheduleNotFoundException() + raise validation.ScheduleNotFoundException() if not repo.schedule_is_owned(db, schedule_id=id, subscriber_id=subscriber.id): - raise ScheduleNotAuthorizedException() + raise validation.ScheduleNotAuthorizedException() if schedule.meeting_link_provider == MeetingLinkProviderType.zoom and subscriber.get_external_connection(ExternalConnectionType.zoom) is None: - raise ZoomNotConnectedException() + raise validation.ZoomNotConnectedException() return repo.update_calendar_schedule(db=db, schedule=schedule, schedule_id=id) @@ -90,17 +88,18 @@ def read_schedule_availabilities( """Returns the calculated availability for the first schedule from a subscribers public profile link""" subscriber = repo.verify_subscriber_link(db, url) if not subscriber: - raise HTTPException(status_code=401, detail=l10n('invalid-link')) + raise validation.InvalidLinkException() + schedules = repo.get_schedules_by_subscriber(db, subscriber_id=subscriber.id) try: schedule = schedules[0] # for now we only process the first existing schedule except IndexError: - raise ScheduleNotFoundException() + raise validation.ScheduleNotFoundException() # check if schedule is enabled if not schedule.active: - raise ScheduleNotFoundException() + raise validation.ScheduleNotFoundException() # calculate theoretically possible slots from schedule config availableSlots = Tools.available_slots_from_schedule(schedule) @@ -109,13 +108,13 @@ def read_schedule_availabilities( calendars = repo.get_calendars_by_subscriber(db, subscriber.id, False) if not calendars or len(calendars) == 0: - raise HTTPException(status_code=404, detail="No calendars found") + raise validation.CalendarNotFoundException() existingEvents = Tools.existing_events_for_schedule(schedule, calendars, subscriber, google_client, db) actualSlots = Tools.events_set_difference(availableSlots, existingEvents) if not actualSlots or len(actualSlots) == 0: - raise HTTPException(status_code=404, detail="No possible booking slots found") + raise validation.SlotNotFoundException() return schemas.AppointmentOut( title=schedule.name, @@ -134,24 +133,27 @@ def request_schedule_availability_slot( """endpoint to request a time slot for a schedule via public link and send confirmation mail to owner""" subscriber = repo.verify_subscriber_link(db, url) if not subscriber: - raise HTTPException(status_code=401, detail="Invalid profile link") + raise validation.InvalidLinkException + schedules = repo.get_schedules_by_subscriber(db, subscriber_id=subscriber.id) try: schedule = schedules[0] # for now we only process the first existing schedule except IndexError: - raise HTTPException(status_code=404, detail="Schedule not found") + raise validation.ScheduleNotFoundException() + # check if schedule is enabled if not schedule.active: - raise HTTPException(status_code=404, detail="Schedule not found") + raise validation.ScheduleNotFoundException() + # get calendar db_calendar = repo.get_calendar(db, calendar_id=schedule.calendar_id) if db_calendar is None: - raise HTTPException(status_code=404, detail="Calendar not found") + raise validation.CalendarNotFoundException() # check if slot still available, might already be taken at this time slot = schemas.SlotBase(**s_a.slot.dict()) if repo.schedule_slot_exists(db, slot, schedule.id): - raise HTTPException(status_code=403, detail="Slot not available") + raise validation.SlotAlreadyTakenException() # create slot in db with token and expiration date token = random_slug() @@ -190,19 +192,22 @@ def decide_on_schedule_availability_slot( """ subscriber = repo.verify_subscriber_link(db, data.owner_url) if not subscriber: - raise HTTPException(status_code=401, detail="Invalid profile link") + raise validation.InvalidLinkException() schedules = repo.get_schedules_by_subscriber(db, subscriber_id=subscriber.id) try: schedule = schedules[0] # for now we only process the first existing schedule except IndexError: - raise HTTPException(status_code=404, detail="Schedule not found") + raise validation.ScheduleNotFoundException() + # check if schedule is enabled if not schedule.active: - raise HTTPException(status_code=404, detail="Schedule not found") + raise validation.ScheduleNotFoundException() + # get calendar calendar = repo.get_calendar(db, calendar_id=schedule.calendar_id) if calendar is None: - raise HTTPException(status_code=404, detail="Calendar not found") + raise validation.CalendarNotFoundException() + # get slot and check if slot exists and is not booked yet and token is the same slot = repo.get_slot(db, data.slot_id) if ( @@ -211,7 +216,8 @@ def decide_on_schedule_availability_slot( or not repo.schedule_has_slot(db, schedule.id, slot.id) or slot.booking_tkn != data.slot_token ): - raise HTTPException(status_code=404, detail="Booking slot not found") + raise validation.SlotNotFoundException() + # TODO: check booking expiration date # check if request was denied if data.confirmed is False: @@ -305,19 +311,22 @@ def schedule_serve_ics( """endpoint to serve ICS file for availability time slot to download""" subscriber = repo.verify_subscriber_link(db, url) if not subscriber: - raise HTTPException(status_code=401, detail="Invalid profile link") + raise validation.InvalidLinkException + schedules = repo.get_schedules_by_subscriber(db, subscriber_id=subscriber.id) try: schedule = schedules[0] # for now we only process the first existing schedule except IndexError: - raise HTTPException(status_code=404, detail="Schedule not found") + raise validation.ScheduleNotFoundException() + # check if schedule is enabled if not schedule.active: - raise HTTPException(status_code=404, detail="Schedule not found") + raise validation.ScheduleNotFoundException() + # get calendar db_calendar = repo.get_calendar(db, calendar_id=schedule.calendar_id) if db_calendar is None: - raise HTTPException(status_code=404, detail="Calendar not found") + raise validation.CalendarNotFoundException() appointment = schemas.AppointmentBase(title=schedule.name, details=schedule.details, location_url=schedule.location_url) return schemas.FileDownload(