Skip to content

Commit

Permalink
auth from ecs api + some code migration
Browse files Browse the repository at this point in the history
  • Loading branch information
voynow committed Nov 13, 2024
1 parent f8a33ad commit 80a6452
Show file tree
Hide file tree
Showing 16 changed files with 257 additions and 36 deletions.
3 changes: 1 addition & 2 deletions api/dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@ RUN pip install --no-cache-dir -r requirements.txt

EXPOSE 80

# Start the app with Python
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "80"]
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "80", "--log-level", "info"]
13 changes: 12 additions & 1 deletion api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ httpx = "^0.27.2"
cryptography = "^43.0.3"
fastapi = "^0.115.4"
uvicorn = "^0.32.0"
python-multipart = "^0.0.17"

[tool.poetry.group.dev.dependencies]
ipykernel = "^6.29.4"
Expand Down
1 change: 1 addition & 0 deletions api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pyjwt==2.9.0 ; python_version >= "3.10" and python_version < "4.0"
pyperclip==1.9.0 ; python_version >= "3.10" and python_version < "4.0"
python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0"
python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0"
python-multipart==0.0.17 ; python_version >= "3.10" and python_version < "4.0"
pytz==2024.2 ; python_version >= "3.10" and python_version < "4.0"
realtime==2.0.6 ; python_version >= "3.10" and python_version < "4.0"
requests==2.32.3 ; python_version >= "3.10" and python_version < "4.0"
Expand Down
66 changes: 65 additions & 1 deletion api/src/auth_manager.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import logging
import os
from datetime import datetime, timezone
from typing import Optional

import jwt
from fastapi import HTTPException, Security
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from src import supabase_client
from src import email_manager, supabase_client
from src.types.user import UserAuthRow, UserRow
from stravalib.client import Client

Expand Down Expand Up @@ -159,3 +160,66 @@ def get_strava_client(athlete_id: int) -> Client:
"""Interface for retrieving a Strava client with valid authentication"""
user_auth = authenticate_athlete(athlete_id)
return get_configured_strava_client(user_auth)


def authenticate_with_code(code: str) -> UserAuthRow:
"""
Authenticate athlete with code, exchange with strava client for token,
generate new JWT, and update database
:param code: temporary authorization code
:return: UserAuthRow
"""
token = strava_client.exchange_code_for_token(
client_id=os.environ["STRAVA_CLIENT_ID"],
client_secret=os.environ["STRAVA_CLIENT_SECRET"],
code=code,
)
strava_client.access_token = token["access_token"]
strava_client.refresh_token = token["refresh_token"]
strava_client.token_expires_at = token["expires_at"]

athlete = strava_client.get_athlete()

jwt_token = generate_jwt(athlete_id=athlete.id, expires_at=token["expires_at"])

user_auth_row = UserAuthRow(
athlete_id=athlete.id,
access_token=strava_client.access_token,
refresh_token=strava_client.refresh_token,
expires_at=strava_client.token_expires_at,
jwt_token=jwt_token,
device_token=supabase_client.get_device_token(athlete.id),
)
supabase_client.upsert_user_auth(user_auth_row)
return user_auth_row


def signup(user_auth: UserAuthRow, email: Optional[str] = None) -> dict:
""" """
email_manager.send_alert_email(
subject="TrackFlow Alert: New Signup Attempt",
text_content=f"You have a new client {email=} attempting to signup",
)
supabase_client.upsert_user(UserRow(athlete_id=user_auth.athlete_id))
return {"success": True, "jwt_token": user_auth.jwt_token, "is_new_user": True}


def authenticate_on_signin(code: str, email: Optional[str] = None) -> dict:
"""
Authenticate with Strava code, and sign up the user if they don't exist.
:param code: Strava authorization code
:param email: User's email (optional)
:return: Dictionary with success status and JWT token
"""
user_auth = authenticate_with_code(code)

if not supabase_client.does_user_exist(user_auth.athlete_id):
return signup(user_auth, email)

return {
"success": True,
"jwt_token": user_auth.jwt_token,
"is_new_user": False,
}
47 changes: 47 additions & 0 deletions api/src/email_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import os
from typing import Dict

import sib_api_v3_sdk
from dotenv import load_dotenv
from urllib3.exceptions import ProtocolError

load_dotenv()

configuration = sib_api_v3_sdk.Configuration()
configuration.api_key["api-key"] = os.environ["EMAIL_API_KEY"]

api_instance = sib_api_v3_sdk.TransactionalEmailsApi(
sib_api_v3_sdk.ApiClient(configuration)
)


def send_alert_email(
subject: str,
text_content: str,
to: Dict[str, str] = {
"name": "Jamie Voynow",
"email": "voynow99@gmail.com",
},
sender: Dict[str, str] = {
"name": "Jamie Voynow",
"email": "voynowtestaddress@gmail.com",
},
) -> sib_api_v3_sdk.CreateSmtpEmail:
"""
Generic template to send alerts/notifications to myself based on miscellaneous events
"""
html_content = f"""
<html>
<body>
<h1>{subject}</h1>
<p>{text_content}</p>
</body>
</html>
"""
send_smtp_email = sib_api_v3_sdk.SendSmtpEmail(
to=[to], html_content=html_content, sender=sender, subject=subject
)
try:
return api_instance.send_transac_email(send_smtp_email)
except ProtocolError:
return api_instance.send_transac_email(send_smtp_email)
48 changes: 23 additions & 25 deletions api/src/main.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import logging
from typing import Optional

from fastapi import BackgroundTasks, Body, Depends, FastAPI, HTTPException, Request
from src import activities, auth_manager, supabase_client
from fastapi import BackgroundTasks, Body, Depends, FastAPI, HTTPException, Request, Form
from src import activities, auth_manager, supabase_client, webhook
from src.types.training_week import TrainingWeek
from src.types.user import UserRow
from src.types.webhook import StravaEvent

app = FastAPI()

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger = logging.getLogger("uvicorn.error")


# health check
@app.get("/health")
async def health():
logger.info("Healthy ✅")
return {"status": "healthy"}


@app.get("/training_week/", response_model=TrainingWeek)
Expand Down Expand Up @@ -121,39 +127,31 @@ async def get_weekly_summaries(


@app.post("/authenticate/")
async def authenticate(code: str, email: Optional[str] = None) -> dict:
async def authenticate(code: str = Form(...)) -> dict:
"""
Authenticate with Strava code and sign up new users
:param code: Strava authorization code
:param email: User's email (optional)
:param code: Strava authorization code from form data
:return: Dictionary with success status, JWT token and new user flag
"""
try:
return auth_manager.authenticate_on_signin(code=code, email=email)
logger.info(f"Authenticating with Strava code: {code}")
return auth_manager.authenticate_on_signin(code=code)
except Exception as e:
logger.error(f"Authentication failed: {e}", exc_info=True)
raise HTTPException(status_code=400, detail=str(e))


def process_strava_event(event: StravaEvent):
"""
Process the Strava webhook event. Perform any updates based on the event data.
@app.post("/strava-webhook/")
async def strava_webhook(request: Request, background_tasks: BackgroundTasks) -> dict:
"""
# Replace this with your Strava-specific logic (e.g., updating training week)
logger.info(f"Processing event: {event}")
# Simulate some processing or call any required functions
# For example, handle_activity_create(user, event)

Handle Strava webhook events
@app.post("/strava-webhook/")
async def strava_webhook(request: Request, background_tasks: BackgroundTasks):
:param request: Webhook request from Strava
:param background_tasks: FastAPI background tasks
:return: Success status
"""
event = await request.json()
logger.info(f"Received Strava webhook event: {event}")

# Validate event and start processing in background
strava_event = StravaEvent(**event)
background_tasks.add_task(process_strava_event, strava_event)

# Immediate response to Strava
return {"status": "received"}
background_tasks.add_task(webhook.maybe_process_strava_event, strava_event)
return {"success": True}
26 changes: 26 additions & 0 deletions api/src/supabase_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,29 @@ def update_preferences(athlete_id: int, preferences: dict):

table = client.table("user")
table.update({"preferences": preferences}).eq("athlete_id", athlete_id).execute()


def upsert_user(user_row: UserRow):
"""
Upsert a row into the user table
:param user_row: An instance of UserRow
"""
row_data = user_row.dict()
if isinstance(row_data["created_at"], datetime.datetime):
row_data["created_at"] = row_data["created_at"].isoformat()

table = client.table("user")
table.upsert(row_data, on_conflict="athlete_id").execute()


def does_user_exist(athlete_id: int) -> bool:
"""
Check if a user exists in the user table
:param athlete_id: The ID of the athlete
:return: True if the user exists, False otherwise
"""
table = client.table("user")
response = table.select("*").eq("athlete_id", athlete_id).execute()
return bool(response.data)
2 changes: 2 additions & 0 deletions api/src/types/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ class StravaEvent(BaseModel):
object_type: str
object_id: int
owner_id: int
event_time: int
updates: dict
45 changes: 45 additions & 0 deletions api/src/webhook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from src import auth_manager, supabase_client
from src.types.webhook import StravaEvent

# from src.training_week import update_training_week
# from src.types.exe_type import ExeType


def handle_activity_create(event: StravaEvent) -> dict:
"""
Handle the creation of a Strava activity
:param event: Strava webhook event
"""
pass
# user = supabase_client.get_user(event.owner_id)
# strava_client = auth_manager.get_strava_client(user.athlete_id)
# activity = strava_client.get_activity(event.object_id)

# if activity.sport_type == "Run":
# return update_training_week(
# user=user,
# exe_type=ExeType.MID_WEEK,
# )
# return {
# "success": False,
# "error": f"Unsupported activity type: {activity.sport_type}",
# }


def maybe_process_strava_event(event: StravaEvent) -> dict:
"""
Process the Strava webhook event. Perform any updates based on the event data.
Strava Event: subscription_id=2****3 aspect_type='create' object_type='activity' object_id=1*********4 owner_id=9******6 event_time=1731515741 updates={}
Strava Event: subscription_id=2****3 aspect_type='update' object_type='activity' object_id=1*********9 owner_id=9******6 event_time=1731515699 updates={'title': 'Best running weather ❄️'}
:param event: Strava webhook event
:return: Success status and error message if any
"""
if event.aspect_type == "create":
return handle_activity_create(event)
else:
return {
"success": False,
"error": f"Unsupported event type: {event.aspect_type}",
}
8 changes: 8 additions & 0 deletions infra/app/ecs/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ resource "aws_ecs_task_definition" "api" {
{
name = "SUPABASE_KEY",
value = var.supabase_key
},
{
name = "EMAIL_API_KEY",
value = var.email_api_key
},
{
name = "OPENAI_API_KEY",
value = var.openai_api_key
}
]
}
Expand Down
12 changes: 11 additions & 1 deletion infra/app/ecs/variable.tf
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,14 @@ variable "supabase_key" {
type = string
description = "Supabase API key"
sensitive = true
}
}
variable "email_api_key" {
type = string
description = "API key for the email service"
sensitive = true
}
variable "openai_api_key" {
type = string
description = "API key for the OpenAI service"
sensitive = true
}
2 changes: 2 additions & 0 deletions infra/app/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ module "ecs" {
image = var.image
supabase_url = var.supabase_url
supabase_key = var.supabase_key
email_api_key = var.email_api_key
openai_api_key = var.openai_api_key
vpc_id = module.network.vpc.id
public_subnet_ids = [for s in module.network.public_subnets : s.id]
depends_on = [module.network]
Expand Down
12 changes: 11 additions & 1 deletion infra/app/variable.tf
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,14 @@ variable "supabase_key" {
type = string
description = "Supabase API key"
sensitive = true
}
}
variable "email_api_key" {
type = string
description = "API key for the email service"
sensitive = true
}
variable "openai_api_key" {
type = string
description = "API key for the OpenAI service"
sensitive = true
}
Loading

0 comments on commit 80a6452

Please sign in to comment.