-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #120 from voynow/119-begin-migration-away-from-lambda
119 begin migration away from lambda
- Loading branch information
Showing
58 changed files
with
2,616 additions
and
198 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
#!/bin/bash | ||
|
||
# Check if Docker is running, and start it if not | ||
if ! docker info >/dev/null 2>&1; then | ||
echo "Starting Docker..." | ||
open --background -a Docker | ||
while ! docker info >/dev/null 2>&1; do | ||
sleep 1 | ||
done | ||
echo "Docker started." | ||
fi | ||
|
||
# Build and push the image | ||
poetry export --without-hashes -f requirements.txt -o requirements.txt | ||
docker build -t trackflow . | ||
docker tag trackflow:latest 498969721544.dkr.ecr.us-east-1.amazonaws.com/trackflow:latest | ||
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 498969721544.dkr.ecr.us-east-1.amazonaws.com | ||
docker push 498969721544.dkr.ecr.us-east-1.amazonaws.com/trackflow:latest | ||
|
||
# Get the image digest | ||
image_digest=$(aws ecr describe-images \ | ||
--repository-name trackflow \ | ||
--image-ids imageTag=latest \ | ||
--query 'imageDetails[0].imageDigest' \ | ||
--output text) | ||
|
||
# Update the image in the Terraform variables | ||
cd ../infra/app | ||
sed -i '' "s|^image = .*|image = \"498969721544.dkr.ecr.us-east-1.amazonaws.com/trackflow@$image_digest\"|" terraform.tfvars | ||
terraform apply -auto-approve | ||
|
||
# Return to the api folder | ||
cd ../../api |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
FROM --platform=linux/amd64 python:3.11-slim | ||
|
||
WORKDIR /app | ||
|
||
COPY . /app | ||
|
||
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"] |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[tool.poetry] | ||
name = "trackflow-api" | ||
version = "0.1.0" | ||
description = "" | ||
authors = ["voynow <voynow99@gmail.com>"] | ||
readme = "README.md" | ||
|
||
[tool.poetry.dependencies] | ||
python = "^3.11" | ||
fastapi = "^0.115.4" | ||
uvicorn = "^0.32.0" | ||
supabase = "^2.10.0" | ||
python-dotenv = "^1.0.1" | ||
pyjwt = "^2.9.0" | ||
stravalib = "^2.1" | ||
|
||
[tool.poetry.group.dev.dependencies] | ||
pytest = "^8.3.3" | ||
|
||
[build-system] | ||
requires = ["poetry-core"] | ||
build-backend = "poetry.core.masonry.api" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
aiohappyeyeballs==2.4.3 ; python_version >= "3.11" and python_version < "4.0" | ||
aiohttp==3.10.10 ; python_version >= "3.11" and python_version < "4.0" | ||
aiosignal==1.3.1 ; python_version >= "3.11" and python_version < "4.0" | ||
annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "4.0" | ||
anyio==4.6.2.post1 ; python_version >= "3.11" and python_version < "4.0" | ||
arrow==1.3.0 ; python_version >= "3.11" and python_version < "4.0" | ||
attrs==24.2.0 ; python_version >= "3.11" and python_version < "4.0" | ||
certifi==2024.8.30 ; python_version >= "3.11" and python_version < "4.0" | ||
charset-normalizer==3.4.0 ; python_version >= "3.11" and python_version < "4.0" | ||
click==8.1.7 ; python_version >= "3.11" and python_version < "4.0" | ||
colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and platform_system == "Windows" | ||
deprecation==2.1.0 ; python_version >= "3.11" and python_version < "4.0" | ||
fastapi==0.115.4 ; python_version >= "3.11" and python_version < "4.0" | ||
flexcache==0.3 ; python_version >= "3.11" and python_version < "4.0" | ||
flexparser==0.4 ; python_version >= "3.11" and python_version < "4.0" | ||
frozenlist==1.5.0 ; python_version >= "3.11" and python_version < "4.0" | ||
gotrue==2.10.0 ; python_version >= "3.11" and python_version < "4.0" | ||
h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0" | ||
h2==4.1.0 ; python_version >= "3.11" and python_version < "4.0" | ||
hpack==4.0.0 ; python_version >= "3.11" and python_version < "4.0" | ||
httpcore==1.0.6 ; python_version >= "3.11" and python_version < "4.0" | ||
httpx==0.27.2 ; python_version >= "3.11" and python_version < "4.0" | ||
httpx[http2]==0.27.2 ; python_version >= "3.11" and python_version < "4.0" | ||
hyperframe==6.0.1 ; python_version >= "3.11" and python_version < "4.0" | ||
idna==3.10 ; python_version >= "3.11" and python_version < "4.0" | ||
multidict==6.1.0 ; python_version >= "3.11" and python_version < "4.0" | ||
packaging==24.2 ; python_version >= "3.11" and python_version < "4.0" | ||
pint==0.24.4 ; python_version >= "3.11" and python_version < "4.0" | ||
platformdirs==4.3.6 ; python_version >= "3.11" and python_version < "4.0" | ||
postgrest==0.18.0 ; python_version >= "3.11" and python_version < "4.0" | ||
propcache==0.2.0 ; python_version >= "3.11" and python_version < "4.0" | ||
pydantic-core==2.23.4 ; python_version >= "3.11" and python_version < "4.0" | ||
pydantic==2.9.2 ; python_version >= "3.11" and python_version < "4.0" | ||
pyjwt==2.9.0 ; python_version >= "3.11" and python_version < "4.0" | ||
python-dateutil==2.9.0.post0 ; python_version >= "3.11" and python_version < "4.0" | ||
python-dotenv==1.0.1 ; python_version >= "3.11" and python_version < "4.0" | ||
pytz==2024.2 ; python_version >= "3.11" and python_version < "4.0" | ||
realtime==2.0.6 ; python_version >= "3.11" and python_version < "4.0" | ||
requests==2.32.3 ; python_version >= "3.11" and python_version < "4.0" | ||
six==1.16.0 ; python_version >= "3.11" and python_version < "4.0" | ||
sniffio==1.3.1 ; python_version >= "3.11" and python_version < "4.0" | ||
starlette==0.41.2 ; python_version >= "3.11" and python_version < "4.0" | ||
storage3==0.9.0 ; python_version >= "3.11" and python_version < "4.0" | ||
stravalib==2.1 ; python_version >= "3.11" and python_version < "4.0" | ||
supabase==2.10.0 ; python_version >= "3.11" and python_version < "4.0" | ||
supafunc==0.7.0 ; python_version >= "3.11" and python_version < "4.0" | ||
types-python-dateutil==2.9.0.20241003 ; python_version >= "3.11" and python_version < "4.0" | ||
typing-extensions==4.12.2 ; python_version >= "3.11" and python_version < "4.0" | ||
urllib3==2.2.3 ; python_version >= "3.11" and python_version < "4.0" | ||
uvicorn==0.32.0 ; python_version >= "3.11" and python_version < "4.0" | ||
websockets==13.1 ; python_version >= "3.11" and python_version < "4.0" | ||
yarl==1.17.1 ; python_version >= "3.11" and python_version < "4.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import logging | ||
import os | ||
from typing import Optional | ||
|
||
import jwt | ||
from fastapi import HTTPException, Security | ||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer | ||
from src import supabase_client | ||
from src.types.user import UserAuthRow, UserRow | ||
from stravalib.client import Client | ||
|
||
logger = logging.getLogger(__name__) | ||
logger.setLevel(logging.INFO) | ||
bearer_scheme = HTTPBearer() | ||
|
||
strava_client = Client() | ||
|
||
|
||
def generate_jwt(athlete_id: int, expires_at: int) -> str: | ||
""" | ||
Generate a JWT token using athlete_id and expiration time, aligning token | ||
expiration cycle with the athlete's Strava token | ||
:param athlete_id: strava internal identifier | ||
:param expires_at: expiration time of strava token | ||
:return: str | ||
""" | ||
payload = {"athlete_id": athlete_id, "exp": expires_at} | ||
token = jwt.encode(payload, os.environ["JWT_SECRET"], algorithm="HS256") | ||
return token | ||
|
||
|
||
def decode_jwt(jwt_token: str, verify_exp: bool = True) -> int: | ||
""" | ||
Decode JWT token and return athlete_id | ||
:param jwt_token: JWT token | ||
:param verify_exp: whether to verify expiration | ||
:return: int if successful, None if decoding fails | ||
:raises: jwt.DecodeError if token is invalid | ||
""" | ||
payload = jwt.decode( | ||
jwt_token, | ||
os.environ["JWT_SECRET"], | ||
algorithms=["HS256"], | ||
options={"verify_exp": verify_exp}, | ||
) | ||
return payload["athlete_id"] | ||
|
||
|
||
def refresh_and_update_user_token(athlete_id: int, refresh_token: str) -> UserAuthRow: | ||
""" | ||
Refresh the user's Strava token and update database | ||
:param athlete_id: strava internal identifier | ||
:param refresh_token: refresh token for Strava API | ||
:return: UserAuthRow | ||
""" | ||
logger.info(f"Refreshing and updating token for athlete {athlete_id}") | ||
access_info = strava_client.refresh_access_token( | ||
client_id=os.environ["STRAVA_CLIENT_ID"], | ||
client_secret=os.environ["STRAVA_CLIENT_SECRET"], | ||
refresh_token=refresh_token, | ||
) | ||
new_jwt_token = generate_jwt( | ||
athlete_id=athlete_id, expires_at=access_info["expires_at"] | ||
) | ||
|
||
user_auth = UserAuthRow( | ||
athlete_id=athlete_id, | ||
access_token=access_info["access_token"], | ||
refresh_token=access_info["refresh_token"], | ||
expires_at=access_info["expires_at"], | ||
jwt_token=new_jwt_token, | ||
device_token=supabase_client.get_device_token(athlete_id), | ||
) | ||
supabase_client.upsert_user_auth(user_auth) | ||
return user_auth | ||
|
||
|
||
def validate_and_refresh_token(token: str) -> int: | ||
""" | ||
Validate and refresh the user's credentials in DB | ||
:param token: JWT token | ||
:return: athlete_id | ||
""" | ||
try: | ||
athlete_id = decode_jwt(token) | ||
except jwt.ExpiredSignatureError: | ||
try: | ||
# If the token is expired, decode athlete_id and refresh | ||
athlete_id = decode_jwt(token, verify_exp=False) | ||
user_auth = supabase_client.get_user_auth(athlete_id) | ||
refresh_and_update_user_token( | ||
athlete_id=athlete_id, refresh_token=user_auth.refresh_token | ||
) | ||
except jwt.DecodeError: | ||
logger.error("Invalid JWT token") | ||
raise HTTPException(status_code=401, detail="Invalid JWT token") | ||
except Exception as e: | ||
logger.error( | ||
f"Unknown error validating and refreshing token: {e}", | ||
exc_info=True, | ||
) | ||
raise HTTPException(status_code=500, detail="Internal server error") | ||
except jwt.DecodeError: | ||
logger.error("Invalid JWT token") | ||
raise HTTPException(status_code=401, detail="Invalid JWT token") | ||
except Exception as e: | ||
logger.error( | ||
f"Unknown error validating and refreshing token: {e}", | ||
exc_info=True, | ||
) | ||
raise HTTPException(status_code=500, detail="Internal server error") | ||
|
||
return athlete_id | ||
|
||
|
||
async def validate_user( | ||
credentials: HTTPAuthorizationCredentials = Security(bearer_scheme), | ||
) -> UserRow: | ||
""" | ||
Dependency that validates the JWT token from the Authorization header | ||
:param credentials: Bearer token credentials | ||
:return: UserRow | ||
""" | ||
athlete_id = validate_and_refresh_token(credentials.credentials) | ||
if athlete_id is None: | ||
logger.error("Invalid authentication credentials") | ||
raise HTTPException( | ||
status_code=401, detail="Invalid authentication credentials" | ||
) | ||
return supabase_client.get_user(athlete_id) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import logging | ||
|
||
from fastapi import Depends, FastAPI, HTTPException | ||
from src import supabase_client | ||
from src.auth_manager import validate_user | ||
from src.types.training_week import TrainingWeek | ||
from src.types.user import UserRow | ||
|
||
app = FastAPI() | ||
|
||
logger = logging.getLogger(__name__) | ||
logger.setLevel(logging.INFO) | ||
|
||
|
||
@app.get("/training_week/", response_model=TrainingWeek) | ||
async def training_week(user: UserRow = Depends(validate_user)): | ||
""" | ||
Retrieve the most recent training_week row by athlete_id | ||
curl -X GET "http://trackflow-alb-499532887.us-east-1.elb.amazonaws.com/training_week/" \ | ||
-H "Authorization: Bearer YOUR_JWT_TOKEN" | ||
:param athlete_id: The athlete_id to retrieve the training_week for | ||
:return: The most recent training_week row for the athlete | ||
""" | ||
try: | ||
return supabase_client.get_training_week(user.athlete_id) | ||
except ValueError as e: | ||
logger.error(f"Error retrieving training week: {e}", exc_info=True) | ||
raise HTTPException(status_code=404, detail=str(e)) |
Oops, something went wrong.