Skip to content

Commit

Permalink
Merge branch 'main' into stage
Browse files Browse the repository at this point in the history
  • Loading branch information
MelissaAutumn committed Nov 17, 2023
2 parents f5af7d0 + 39bf909 commit 88f57b8
Show file tree
Hide file tree
Showing 34 changed files with 1,001 additions and 226 deletions.
12 changes: 12 additions & 0 deletions .aws/task-definition.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@
{
"name": "SENTRY_DSN",
"value": "https://5dddca3ecc964284bb8008bc2beef808@o4505428107853824.ingest.sentry.io/4505428124827648"
},
{
"name": "ZOOM_API_ENABLED",
"value": true
},
{
"name": "ZOOM_AUTH_CALLBACK",
"value": "https://appointment.day/zoom/callback"
}
],
"secrets": [
Expand All @@ -73,6 +81,10 @@
{
"name": "GOOGLE_OAUTH_SECRETS",
"valueFrom": "arn:aws:secretsmanager:us-east-1:768512802988:secret:staging/appointment/google-cal-oauth-VevaSo"
},
{
"name": "ZOOM_SECRETS",
"valueFrom": "arn:aws:secretsmanager:us-east-1:768512802988:secret:staging/appointment/zoom-S862zi"
}
],
"mountPoints": [],
Expand Down
158 changes: 84 additions & 74 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,74 +1,84 @@
# Appointment backend configuration.

# -- GENERAL --
# Logging level: DEBUG|INFO|WARNING|ERROR|CRITICAL
LOG_LEVEL=ERROR
LOG_USE_STREAM=1

# -- FRONTEND --
FRONTEND_URL=http://localhost:8080
# Leave blank for no short url
SHORT_BASE_URL=

# -- DATABASE --
DATABASE_URL=
DATABASE_SECRETS=
# Secret phrase for database encryption (e.g. create it by running `openssl rand -hex 32`)
DB_SECRET=

# -- AUTH0 --
# Management API
AUTH0_API_CLIENT_ID=
AUTH0_API_SECRET=
# Auth API
AUTH0_API_DOMAIN=
AUTH0_API_AUDIENCE=
# Role keys, configurable in Auth0 User Management -> Roles
AUTH0_API_ROLE_ADMIN=
AUTH0_API_ROLE_BASIC=
AUTH0_API_ROLE_PLUS=
AUTH0_API_ROLE_PRO=

# -- MAIL --
# Connection security: SSL|STARTTLS|NONE
SMTP_SECURITY=SSL
# Address and port of the SMTP server
SMTP_URL=
SMTP_PORT=
# SMTP user credentials
SMTP_USER=
SMTP_PASS=
# Authorized email address for sending emails
SMTP_SENDER=

# -- TIERS --
# Max number of calendars to be simultanously connected for members of the basic tier
TIER_BASIC_CALENDAR_LIMIT=3
# Max number of calendars to be simultanously connected for members of the plus tier
TIER_PLUS_CALENDAR_LIMIT=5
# Max number of calendars to be simultanously connected for members of the pro tier
TIER_PRO_CALENDAR_LIMIT=10

# -- GOOGLE AUTH --
GOOGLE_AUTH_CLIENT_ID=
GOOGLE_AUTH_SECRET=
GOOGLE_AUTH_PROJECT_ID=
GOOGLE_AUTH_CALLBACK=http://localhost:5000/google/callback

# -- SIGNED URL SECRET --
# Shared secret for url signing (e.g. create it by running `openssl rand -hex 32`)
SIGNED_SECRET=
# If empty, sentry will be disabled
SENTRY_DSN=
# Possible values: prod, dev
APP_ENV=dev

# -- TESTING --
AUTH0_TEST_USER=
AUTH0_TEST_PASS=
CALDAV_TEST_PRINCIPAL_URL=
CALDAV_TEST_CALENDAR_URL=
CALDAV_TEST_USER=
CALDAV_TEST_PASS=
GOOGLE_TEST_USER=
GOOGLE_TEST_PASS=
# Appointment backend configuration.

# -- GENERAL --
# Logging level: DEBUG|INFO|WARNING|ERROR|CRITICAL
LOG_LEVEL=ERROR
LOG_USE_STREAM=1

# -- FRONTEND --
FRONTEND_URL=http://localhost:8080
# Leave blank for no short url
SHORT_BASE_URL=

# -- DATABASE --
DATABASE_URL=
DATABASE_SECRETS=
# Secret phrase for database encryption (e.g. create it by running `openssl rand -hex 32`)
DB_SECRET=

# -- AUTH0 --
# Management API
AUTH0_API_CLIENT_ID=
AUTH0_API_SECRET=
# Auth API
AUTH0_API_DOMAIN=
AUTH0_API_AUDIENCE=
# Role keys, configurable in Auth0 User Management -> Roles
AUTH0_API_ROLE_ADMIN=
AUTH0_API_ROLE_BASIC=
AUTH0_API_ROLE_PLUS=
AUTH0_API_ROLE_PRO=

# -- MAIL --

# Service email for emails on behalf of Thunderbird Appointment
SERVICE_EMAIL=[email protected]

# Connection security: SSL|STARTTLS|NONE
SMTP_SECURITY=SSL
# Address and port of the SMTP server
SMTP_URL=
SMTP_PORT=
# SMTP user credentials
SMTP_USER=
SMTP_PASS=
# Authorized email address for sending emails, leave empty to default to organizer
SMTP_SENDER=

# -- TIERS --
# Max number of calendars to be simultanously connected for members of the basic tier
TIER_BASIC_CALENDAR_LIMIT=3
# Max number of calendars to be simultanously connected for members of the plus tier
TIER_PLUS_CALENDAR_LIMIT=5
# Max number of calendars to be simultanously connected for members of the pro tier
TIER_PRO_CALENDAR_LIMIT=10

# -- GOOGLE AUTH --
GOOGLE_AUTH_CLIENT_ID=
GOOGLE_AUTH_SECRET=
GOOGLE_AUTH_PROJECT_ID=
GOOGLE_AUTH_CALLBACK=http://localhost:5000/google/callback

# -- Zoom API --
ZOOM_API_ENABLED=False
ZOOM_AUTH_CLIENT_ID=
ZOOM_AUTH_SECRET=
ZOOM_AUTH_CALLBACK=http://localhost:8090/zoom/callback

# -- SIGNED URL SECRET --
# Shared secret for url signing (e.g. create it by running `openssl rand -hex 32`)
SIGNED_SECRET=
# If empty, sentry will be disabled
SENTRY_DSN=
# Possible values: prod, dev
APP_ENV=dev

# -- TESTING --
AUTH0_TEST_USER=
AUTH0_TEST_PASS=
CALDAV_TEST_PRINCIPAL_URL=
CALDAV_TEST_CALENDAR_URL=
CALDAV_TEST_USER=
CALDAV_TEST_PASS=
GOOGLE_TEST_USER=
GOOGLE_TEST_PASS=
2 changes: 2 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ sqlalchemy-utils==0.39.0
sqlalchemy==1.4.40
uvicorn==0.20.0
validators==0.20.0
oauthlib==3.2.2
requests-oauthlib==1.3.1
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from google_auth_oauthlib.flow import Flow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from ..database import repo
from ..database.models import CalendarProvider
from ..database.schemas import CalendarConnection
from ..exceptions.google_api import GoogleScopeChanged, GoogleInvalidCredentials
from ...database import repo
from ...database.models import CalendarProvider
from ...database.schemas import CalendarConnection
from ...exceptions.google_api import GoogleScopeChanged, GoogleInvalidCredentials


class GoogleClient:
Expand Down
91 changes: 91 additions & 0 deletions backend/src/appointment/controller/apis/zoom_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import json

from requests_oauthlib import OAuth2Session
from ...database import models, repo
from ...database.database import SessionLocal


class ZoomClient:
OAUTH_AUTHORIZATION_URL = "https://zoom.us/oauth/authorize"
OAUTH_DEVICE_AUTHORIZATION_URL = "https://zoom.us/oauth/devicecode"
OAUTH_TOKEN_URL = "https://zoom.us/oauth/token"
OAUTH_DEVICE_VERIFY_URL = "https://zoom.us/oauth_device"
OAUTH_REQUEST_URL = "https://api.zoom.us/v2"

SCOPES = [
"user:read",
"user_info:read",
"meeting:write"
]

client: OAuth2Session | None = None
subscriber_id: int | None = None

def __init__(self, client_id, client_secret, callback_url):
self.client_id = client_id
self.client_secret = client_secret
self.callback_url = callback_url
self.subscriber_id = None
self.client = None

def setup(self, subscriber_id=None, token=None):
"""Setup our oAuth session"""
if type(token) is str:
token = json.loads(token)

self.subscriber_id = subscriber_id
self.client = OAuth2Session(self.client_id, redirect_uri=self.callback_url, scope=self.SCOPES,
auto_refresh_url=self.OAUTH_TOKEN_URL,
auto_refresh_kwargs={"client_id": self.client_id, "client_secret": self.client_secret},
token=token,
token_updater=self.token_saver)

pass

def get_redirect_url(self, state):
url, state = self.client.authorization_url(self.OAUTH_AUTHORIZATION_URL, state=state)

return url, state

def get_credentials(self, code: str):
return self.client.fetch_token(self.OAUTH_TOKEN_URL, code, client_secret=self.client_secret, include_client_id=True)

def token_saver(self, token):
"""requests-oauth automagically calls this function when it has a new refresh token for us.
This makes it a bit awkward but we make it work..."""
self.client.token = token

# Need a subscriber attached to this request in order to save a token
if self.subscriber_id is None:
return

with SessionLocal() as db:
repo.update_subscriber_external_connection_token(db, json.dumps(token), self.subscriber_id, models.ExternalConnectionType.zoom)

def get_me(self):
return self.client.get(f'{self.OAUTH_REQUEST_URL}/users/me').json()

def create_meeting(self, title, start_time, duration, timezone = None):
# https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetingCreate

response = self.client.post(f'{self.OAUTH_REQUEST_URL}/users/me/meetings', json={
'type': 2, # Scheduled Meeting
'default_password': True,
'duration': duration,
'start_time': f"{start_time}Z", # Make it UTC
'topic': title[:200], # Max 200 chars
'settings': {
'private_meeting': True,
'registrants_confirmation_email': False,
'registrants_email_notification': False,
}
})

response.raise_for_status()

return response.json()

def get_meeting(self, meeting_id):
response = self.client.get(f'{self.OAUTH_REQUEST_URL}/meetings/{meeting_id}')
response.raise_for_status()
return response.json()
10 changes: 8 additions & 2 deletions backend/src/appointment/controller/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
Handle connection to a CalDAV server.
"""
import json
import logging
from caldav import DAVClient
from google.oauth2.credentials import Credentials
from icalendar import Calendar, Event, vCalAddress, vText
from datetime import datetime, timedelta, timezone
from dateutil.parser import parse

from .google_client import GoogleClient
from .apis.google_client import GoogleClient
from ..database import schemas
from ..database.models import CalendarProvider
from ..controller.mailer import Attachment, InvitationMail
Expand Down Expand Up @@ -264,6 +263,13 @@ def create_vevent(
event.add("dtstamp", datetime.utcnow())
event["description"] = appointment.details
event["organizer"] = org

# Prefer the slot meeting link url over the appointment location url
location_url = slot.meeting_link_url if slot.meeting_link_url is not None else appointment.location_url

if location_url != "" or location_url is not None:
event.add('location', location_url)

cal.add_component(event)
return cal.to_ical()

Expand Down
18 changes: 17 additions & 1 deletion backend/src/appointment/controller/mailer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import jinja2
import validators

from datetime import datetime
from html import escape
from email import encoders
from email.mime.base import MIMEBase
Expand Down Expand Up @@ -139,6 +138,23 @@ def html(self):
return get_template("invite.jinja2").render()


class ZoomMeetingFailedMail(Mailer):
def __init__(self, appointment_title, *args, **kwargs):
"""init Mailer with invitation specific defaults"""
defaultKwargs = {
"subject": "[TBA] Zoom Meeting Link Creation Error",
}
super(ZoomMeetingFailedMail, self).__init__(*args, **defaultKwargs, **kwargs)

self.appointment_title = appointment_title

def html(self):
return get_template("errors/zoom_invite_failed.jinja2").render(title=self.appointment_title)

def text(self):
return f"Unfortunately there was an error creating your Zoom meeting for your upcoming appointment: {self.appointment_title}"


class ConfirmationMail(Mailer):
def __init__(self, confirmUrl, denyUrl, attendee, date, *args, **kwargs):
"""init Mailer with confirmation specific defaults"""
Expand Down
Loading

0 comments on commit 88f57b8

Please sign in to comment.