Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chores/CS and linter fixes backend #457

Merged
merged 10 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ line-length = 120
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

# Assume Python 3.10.
# TODO: Assume Python 3.10.
devmount marked this conversation as resolved.
Show resolved Hide resolved
target-version = "py310"

[tool.ruff.mccabe]
Expand Down
2 changes: 0 additions & 2 deletions backend/src/appointment/commands/create_invite_codes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import os

from ..database import repo
from ..dependencies.database import get_engine_and_session

Expand Down
18 changes: 10 additions & 8 deletions backend/src/appointment/commands/download_legal.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,35 @@


def run():
"""Helper function to update privacy and terms. Please check to ensure you're not getting a 404 before committing lol."""
"""Helper function to update privacy and terms.
Please check to ensure you're not getting a 404 before committing lol.
"""
print("Downloading the latest legal documents...")

extensions = ['markdown.extensions.attr_list']
devmount marked this conversation as resolved.
Show resolved Hide resolved
extensions = ["markdown.extensions.attr_list"]
# Only english for now. There's no german TB privacy policy?
locales = ['en']
locales = ["en"]

for locale in locales:
privacy_policy = os.getenv('TBA_PRIVACY_POLICY_URL').format(locale=locale)
terms_of_use = os.getenv('TBA_TERMS_OF_USE_URL').format(locale=locale)
privacy_policy = os.getenv("TBA_PRIVACY_POLICY_URL").format(locale=locale)
terms_of_use = os.getenv("TBA_TERMS_OF_USE_URL").format(locale=locale)

os.makedirs(f'{os.path.dirname(__file__)}/../tmp/legal/{locale}', exist_ok=True)
os.makedirs(f"{os.path.dirname(__file__)}/../tmp/legal/{locale}", exist_ok=True)

if privacy_policy:
print("Privacy policy url found.")
contents = requests.get(privacy_policy).text
html = markupsafe.Markup(markdown.markdown(contents, extensions=extensions))

with open(f'{os.path.dirname(__file__)}/../tmp/legal/{locale}/privacy.html', 'w') as fh:
with open(f"{os.path.dirname(__file__)}/../tmp/legal/{locale}/privacy.html", "w") as fh:
fh.write(html)

if terms_of_use:
print("Terms of use url found.")
contents = requests.get(terms_of_use).text
html = markupsafe.Markup(markdown.markdown(contents, extensions=extensions))

with open(f'{os.path.dirname(__file__)}/../tmp/legal/{locale}/terms.html', 'w') as fh:
with open(f"{os.path.dirname(__file__)}/../tmp/legal/{locale}/terms.html", "w") as fh:
fh.write(html)

print("Done! Copy them over to the frontend/src/assets/legal!")
3 changes: 1 addition & 2 deletions backend/src/appointment/commands/update_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,4 @@ def run():
command.stamp(alembic_cfg, "head")
else:
print("Database already initialized, running migrations")
command.upgrade(alembic_cfg, 'head')

command.upgrade(alembic_cfg, "head")
98 changes: 57 additions & 41 deletions backend/src/appointment/controller/apis/fxa_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,25 @@ def from_url(url):
response: dict = requests.get(url).json()

# Check our supported scopes
scopes = response.get('scopes_supported')
if 'profile' not in scopes:
scopes = response.get("scopes_supported")
if "profile" not in scopes:
logging.warning("Profile scope not found in supported scopes for fxa!")

config = FxaConfig()
config.issuer = response.get('issuer')
config.authorization_url = response.get('authorization_endpoint')
config.issuer = response.get("issuer")
config.authorization_url = response.get("authorization_endpoint")
# Not available from the config endpoint, but it's on the same domain as authorization
config.metrics_flow_url = response.get('authorization_endpoint').replace('authorization', 'metrics-flow')
config.token_url = response.get('token_endpoint')
config.profile_url = response.get('userinfo_endpoint')
config.destroy_url = response.get('revocation_endpoint')
config.jwks_url = response.get('jwks_uri')
config.metrics_flow_url = response.get("authorization_endpoint").replace("authorization", "metrics-flow")
config.token_url = response.get("token_endpoint")
config.profile_url = response.get("userinfo_endpoint")
config.destroy_url = response.get("revocation_endpoint")
config.jwks_url = response.get("jwks_uri")

return config


class FxaClient:
ENTRYPOINT = 'tbappointment'
ENTRYPOINT = "tbappointment"

SCOPES = [
"profile",
Expand All @@ -61,17 +61,25 @@ def __init__(self, client_id, client_secret, callback_url):

def setup(self, subscriber_id=None, token=None):
"""Retrieve the openid connect urls, and setup our client connection"""
if type(token) is str:
if isinstance(token, str):
token = json.loads(token)

self.config = FxaConfig.from_url(os.getenv('FXA_OPEN_ID_CONFIG'))
self.config = FxaConfig.from_url(os.getenv("FXA_OPEN_ID_CONFIG"))

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

def is_in_allow_list(self, db, email: str):
"""Check this email against our allow list"""
Expand All @@ -81,12 +89,12 @@ def is_in_allow_list(self, db, email: str):
if subscriber:
return True

allow_list = os.getenv('FXA_ALLOW_LIST')
allow_list = os.getenv("FXA_ALLOW_LIST")
# If we have no allow list, then we allow everyone
if not allow_list or allow_list == '':
if not allow_list or allow_list == "":
return True

return email.endswith(tuple(allow_list.split(',')))
return email.endswith(tuple(allow_list.split(",")))

def get_redirect_url(self, db, state, email):
if not self.is_in_allow_list(db, email):
Expand All @@ -96,39 +104,46 @@ def get_redirect_url(self, db, state, email):
utm_source = "login"

try:
response = self.client.get(url=self.config.metrics_flow_url, params={
'entrypoint': self.ENTRYPOINT,
'form_type': 'email',
'utm_campaign': utm_campaign,
'utm_source': utm_source
})
response = self.client.get(
url=self.config.metrics_flow_url,
params={
"entrypoint": self.ENTRYPOINT,
"form_type": "email",
"utm_campaign": utm_campaign,
"utm_source": utm_source,
},
)

response.raise_for_status()

flow_values = response.json()
except requests.HTTPError as e:
# Not great, but we can still continue along..
logging.error(f"Could not initialize metrics flow, error occurred: {e.response.status_code} - {e.response.text}")
logging.error(
f"Could not initialize metrics flow, error occurred: {e.response.status_code} - {e.response.text}"
)
flow_values = {}

url, state = self.client.authorization_url(
self.config.authorization_url,
state=state,
access_type='offline',
access_type="offline",
entrypoint=self.ENTRYPOINT,
action='email',
action="email",
# Flow metrics stuff
email=email,
flow_begin_time=flow_values.get('flowBeginTime'),
flow_id=flow_values.get('flowId'),
flow_begin_time=flow_values.get("flowBeginTime"),
flow_id=flow_values.get("flowId"),
utm_source=utm_source,
utm_campaign=utm_campaign
utm_campaign=utm_campaign,
)

return url, state

def get_credentials(self, code: str):
return self.client.fetch_token(self.config.token_url, code, client_secret=self.client_secret, include_client_id=True)
return self.client.fetch_token(
self.config.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.
Expand All @@ -141,7 +156,9 @@ def token_saver(self, token):
if self.subscriber_id is None:
return

repo.external_connection.update_token(next(get_db()), json.dumps(token), self.subscriber_id, models.ExternalConnectionType.fxa)
repo.external_connection.update_token(
next(get_db()), json.dumps(token), self.subscriber_id, models.ExternalConnectionType.fxa
)

def get_profile(self):
"""Retrieve the user's profile information"""
Expand All @@ -150,22 +167,21 @@ def get_profile(self):
def logout(self):
"""Invalidate the current refresh token"""
# I assume a refresh token will destroy its access tokens
refresh_token = self.client.token.get('refresh_token')
refresh_token = self.client.token.get("refresh_token")

if refresh_token is None:
raise MissingRefreshTokenException()

# This route doesn't want auth! (Because we're destroying it)
resp = requests.post(self.config.destroy_url, json={
'refresh_token': refresh_token,
'client_id': self.client_id,
'client_secret': self.client_secret
})
resp = requests.post(
self.config.destroy_url,
json={"refresh_token": refresh_token, "client_id": self.client_id, "client_secret": self.client_secret},
)

resp.raise_for_status()
return resp

def get_jwk(self) -> Dict:
"""Retrieve the keys object on the jwks url"""
response = requests.get(self.config.jwks_url).json()
return response.get('keys', [])
return response.get("keys", [])
44 changes: 21 additions & 23 deletions backend/src/appointment/controller/apis/google_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def get_profile(self, token):
if self.client is None:
return None

user_info_service = build('oauth2', 'v2', credentials=token)
user_info_service = build("oauth2", "v2", credentials=token)
user_info = user_info_service.userinfo().get().execute()

return user_info
Expand All @@ -83,12 +83,12 @@ def list_calendars(self, token):
response = {}
items = []
with build("calendar", "v3", credentials=token, cache_discovery=False) as service:
request = service.calendarList().list(minAccessRole='writer')
request = service.calendarList().list(minAccessRole="writer")
while request is not None:
try:
response = request.execute()

items += response.get('items', [])
items += response.get("items", [])
except HttpError as e:
logging.warning(f"[google_client.list_calendars] Request Error: {e.status_code}/{e.error_details}")

Expand All @@ -101,37 +101,38 @@ def list_events(self, calendar_id, time_min, time_max, token):
items = []

# Limit the fields we request
fields = ','.join(
fields = ",".join(
(
'items/status',
'items/summary',
'items/description',
'items/attendees',
'items/start',
'items/end',
"items/status",
"items/summary",
"items/description",
"items/attendees",
"items/start",
"items/end",
# Top level stuff
'nextPageToken',
"nextPageToken",
)
)

# Explicitly ignore workingLocation events
# See: https://developers.google.com/calendar/api/v3/reference/events#eventType
event_types = [
'default',
'focusTime',
'outOfOffice'
]
event_types = ["default", "focusTime", "outOfOffice"]

with build("calendar", "v3", credentials=token, cache_discovery=False) as service:
request = service.events().list(
calendarId=calendar_id, timeMin=time_min, timeMax=time_max, singleEvents=True, orderBy="startTime",
eventTypes=event_types, fields=fields
calendarId=calendar_id,
timeMin=time_min,
timeMax=time_max,
singleEvents=True,
orderBy="startTime",
eventTypes=event_types,
fields=fields,
)
while request is not None:
try:
response = request.execute()

items += response.get('items', [])
items += response.get("items", [])
except HttpError as e:
logging.warning(f"[google_client.list_events] Request Error: {e.status_code}/{e.error_details}")

Expand Down Expand Up @@ -170,10 +171,7 @@ def sync_calendars(self, db, subscriber_id: int, token):
# add calendar
try:
repo.calendar.update_or_create(
db=db,
calendar=cal,
calendar_url=calendar.get("id"),
subscriber_id=subscriber_id
db=db, calendar=cal, calendar_url=calendar.get("id"), subscriber_id=subscriber_id
)
except Exception as err:
logging.warning(
Expand Down
Loading
Loading