Skip to content

Commit 9f52665

Browse files
authored
Refactoring (#25)
* add rest if env vars to settings * updates to db * more improvments * cleanup * update naming * bugfix * change scope * update descriptions * . * simplify dockerfile * bump checkout * . * . * . * fixes * finalise
1 parent 25a8869 commit 9f52665

File tree

16 files changed

+202
-167
lines changed

16 files changed

+202
-167
lines changed

.github/workflows/build-push-docker-image.yml

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,20 @@ jobs:
1515
packages: write
1616

1717
steps:
18-
- uses: actions/checkout@v3
19-
- name: Install dependencies
18+
- uses: actions/checkout@v4
19+
- uses: 'google-github-actions/auth@v2'
20+
with:
21+
credentials_json: ${{ secrets.GCP_CREDENTIALS }}
22+
- name: Setup Google Cloud SDK
23+
uses: google-github-actions/setup-gcloud@v1
24+
25+
- name: Login
2026
run: |
21-
python -m pip install --upgrade pip
22-
pip install uv
23-
- name: Build the Docker image
27+
28+
gcloud auth configure-docker ${{secrets.GCP_REGION}}-docker.pkg.dev
29+
30+
31+
- name: Build and push the Docker image
2432
run: |
25-
uv pip compile pyproject.toml -o requirements.txt
26-
docker build . -t ghcr.io/${{ github.repository }}:latest
27-
- name: Log in to GitHub Container Registry
28-
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
29-
- name: Push the Docker image
30-
run: docker push ghcr.io/${{ github.repository }}:latest
33+
docker build -t ${{secrets.GCP_REGION}}-docker.pkg.dev/${{secrets.GCP_PROJECT_ID}}/strava/strava:latest .
34+
docker push ${{secrets.GCP_REGION}}-docker.pkg.dev/${{secrets.GCP_PROJECT_ID}}/strava/strava:latest

Dockerfile

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,8 @@ FROM python:3.11-slim-bookworm
22

33
WORKDIR /app
44

5-
RUN apt-get update && apt-get install -y --no-install-recommends \
6-
build-essential \
7-
libpq-dev \
8-
libgeos-dev \
9-
libopenblas-dev \
10-
libatlas-base-dev \
11-
12-
&& rm -rf /var/lib/apt/lists/*
13-
145
RUN pip install uv==0.6.3
156

16-
177
ENV UV_PROJECT_ENVIRONMENT="/uv_venv/"
188
ENV PATH="/${UV_PROJECT_ENVIRONMENT}/bin:$PATH"
199

docker-compose.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ services:
1717
- PUSHBULLET_API_KEY=${PUSHBULLET_API_KEY}
1818
- POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@postgres:5432/postgres
1919
- STRAVA_VERIFY_TOKEN=${STRAVA_VERIFY_TOKEN}
20-
image: ghcr.io/niklasvm/strava:latest
20+
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
21+
build:
22+
context: .
23+
dockerfile: Dockerfile
2124
ports:
2225
- 80:8000
2326
depends_on:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ description = "Add your description here"
55
readme = "README.md"
66
requires-python = ">=3.11"
77
dependencies = [
8+
"cryptography>=44.0.2",
89
"dill>=0.3.9",
910
"fastapi[standard]>=0.115.8",
1011
"folium>=0.19.4",

src/app/apis/auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async def authorization() -> RedirectResponse:
3131
url = client.authorization_url(
3232
client_id=settings.strava_client_id,
3333
redirect_uri=redirect_uri,
34-
scope=["activity:read_all", "activity:write"],
34+
scope=["activity:read", "activity:write"],
3535
)
3636

3737
# redirect to strava authorization url
@@ -55,6 +55,7 @@ async def login(
5555
client_id=settings.strava_client_id,
5656
client_secret=settings.strava_client_secret,
5757
postgres_connection_string=settings.postgres_connection_string,
58+
encryption_key=settings.encryption_key,
5859
)
5960
uuid = athlete.uuid
6061
except Exception:

src/app/apis/webhook.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ async def handle_post_event(
3030
settings.strava_client_id,
3131
settings.strava_client_secret,
3232
settings.postgres_connection_string,
33+
settings.gemini_api_key,
34+
settings.pushbullet_api_key,
35+
settings.encryption_key,
3336
)
3437

3538
return JSONResponse(content={"message": "Received webhook event"}, status_code=200)

src/app/core/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ class Settings(BaseModel):
1111
strava_verify_token: str
1212
application_url: str
1313
postgres_connection_string: str
14+
gemini_api_key: str
15+
pushbullet_api_key: str
16+
encryption_key: bytes
1417

1518
@field_validator("*")
1619
def not_empty(cls, value):
@@ -26,6 +29,9 @@ def not_empty(cls, value):
2629
strava_verify_token=os.environ["STRAVA_VERIFY_TOKEN"],
2730
application_url=os.environ["APPLICATION_URL"],
2831
postgres_connection_string=os.environ["POSTGRES_CONNECTION_STRING"],
32+
gemini_api_key=os.environ["GEMINI_API_KEY"],
33+
pushbullet_api_key=os.environ["PUSHBULLET_API_KEY"],
34+
encryption_key=os.environ["ENCRYPTION_KEY"].encode(),
2935
)
3036
except ValueError as e:
3137
print(f"Configuration error: {e}")

src/app/db/adapter.py

Lines changed: 71 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,91 @@
11
import datetime
22
import logging
3-
import uuid
43
from sqlalchemy import create_engine
54

65
from sqlalchemy.orm import sessionmaker
6+
from cryptography.fernet import Fernet
7+
import base64
78

8-
from src.app.db.models import Auth, Base, Athlete
9-
from stravalib.model import SummaryAthlete
9+
from src.app.db.models import Auth, Base, User
1010

1111
logging.basicConfig(level=logging.INFO)
1212
logger = logging.getLogger(__name__)
1313

1414

15+
def encrypt_token(token: str, key: bytes) -> str:
16+
"""Encrypts an access token."""
17+
f = Fernet(key)
18+
encrypted_token = f.encrypt(token.encode())
19+
return base64.urlsafe_b64encode(encrypted_token).decode()
20+
21+
22+
def decrypt_token(encrypted_token: str, key: bytes) -> str:
23+
"""Decrypts an access token."""
24+
f = Fernet(key)
25+
encrypted_token_bytes = base64.urlsafe_b64decode(encrypted_token)
26+
decrypted_token = f.decrypt(encrypted_token_bytes).decode()
27+
return decrypted_token
28+
29+
1530
class Database:
16-
def __init__(self, connection_string: str):
31+
def __init__(self, connection_string: str, encryption_key: bytes):
1732
engine = create_engine(connection_string)
1833
Base.metadata.create_all(engine) # create the tables.
19-
Session = sessionmaker(bind=engine)
20-
self.session = Session()
21-
22-
def add_athlete(self, athlete: SummaryAthlete):
23-
found_athlete = (
24-
self.session.query(Athlete).filter(Athlete.athlete_id == athlete.id).first()
25-
)
26-
if not found_athlete:
27-
id = str(uuid.uuid4())
28-
kwargs = {
29-
k: v
30-
for k, v in athlete.__dict__.items()
31-
if k in Athlete.__table__.columns.keys()
32-
}
33-
34-
kwargs["created_at"] = datetime.datetime.now()
35-
kwargs["updated_at"] = datetime.datetime.now()
36-
kwargs["athlete_id"] = athlete.id
37-
38-
kwargs["uuid"] = id
39-
40-
athlete_model = Athlete(**kwargs)
41-
self.session.add(athlete_model)
42-
self.session.commit()
43-
logger.info(f"Added athlete {athlete.id} to the database")
44-
45-
return id
46-
else:
47-
logger.info(f"Athlete with id {athlete.id} already exists in the database")
48-
return found_athlete.uuid
49-
50-
def get_athlete(self, uuid: str) -> Athlete:
51-
return self.session.query(Athlete).filter(Athlete.uuid == uuid).first()
52-
53-
def add_auth(
54-
self,
55-
athlete_id: int,
56-
access_token: str,
57-
refresh_token: str,
58-
expires_at: int,
59-
scope: str,
60-
):
61-
if self.session.query(Auth).filter(Auth.athlete_id == athlete_id).first():
62-
self.session.query(Auth).filter(Auth.athlete_id == athlete_id).update(
63-
{
64-
"access_token": access_token,
65-
"refresh_token": refresh_token,
66-
"expires_at": expires_at,
67-
"scope": scope,
68-
"updated_at": datetime.datetime.now(),
69-
}
34+
self.Session = sessionmaker(bind=engine)
35+
36+
self.encryption_key = encryption_key
37+
38+
def add_user(self, user: User):
39+
with self.Session() as session:
40+
existing_user = (
41+
session.query(User).filter(User.athlete_id == user.athlete_id).first()
7042
)
71-
self.session.commit()
72-
logger.info(f"Updated auth for athlete {athlete_id}")
73-
74-
else:
75-
auth = Auth(
76-
uuid=str(uuid.uuid4()),
77-
athlete_id=athlete_id,
78-
access_token=access_token,
79-
refresh_token=refresh_token,
80-
expires_at=expires_at,
81-
scope=scope,
82-
created_at=datetime.datetime.now(),
83-
updated_at=datetime.datetime.now(),
43+
if not existing_user:
44+
session.add(user)
45+
session.commit()
46+
logger.info(f"Added user {user.uuid} to the database")
47+
48+
return str(user.uuid)
49+
50+
logger.info(
51+
f"User with id {existing_user.uuid} already exists in the database"
8452
)
85-
self.session.add(auth)
86-
self.session.commit()
87-
logger.info(f"Added auth for athlete {athlete_id}")
53+
return str(existing_user.uuid)
54+
55+
def get_user(self, uuid: str) -> User:
56+
with self.Session() as session:
57+
return session.query(User).filter(User.uuid == uuid).first()
8858

89-
def get_auth(self, athlete_id: int) -> Auth:
90-
return self.session.query(Auth).filter(Auth.athlete_id == athlete_id).first()
59+
def add_auth(self, auth: Auth):
60+
# encrypt tokens
61+
auth.access_token = encrypt_token(auth.access_token, self.encryption_key)
62+
auth.refresh_token = encrypt_token(auth.refresh_token, self.encryption_key)
63+
64+
with self.Session() as session:
65+
existing_auth = (
66+
session.query(Auth).filter(Auth.athlete_id == auth.athlete_id).first()
67+
)
68+
if existing_auth:
69+
existing_auth.access_token = auth.access_token
70+
existing_auth.refresh_token = auth.refresh_token
71+
existing_auth.expires_at = auth.expires_at
72+
existing_auth.scope = auth.scope
73+
existing_auth.updated_at = datetime.datetime.now()
74+
75+
session.commit()
76+
logger.info(f"Updated auth for athlete {auth.athlete_id}")
77+
78+
else:
79+
session.add(auth)
80+
session.commit()
81+
logger.info(f"Added auth for athlete {auth.athlete_id}")
82+
83+
def get_auth_by_athlete_id(self, athlete_id: int) -> Auth:
84+
with self.Session() as session:
85+
auth = session.query(Auth).filter(Auth.athlete_id == athlete_id).first()
86+
auth.access_token = decrypt_token(auth.access_token, self.encryption_key)
87+
auth.refresh_token = decrypt_token(auth.refresh_token, self.encryption_key)
88+
return auth
9189

9290
# def add_activity(self, activity: SummaryActivity):
9391
# activity_dict = activity.model_dump()

src/app/db/models.py

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
from __future__ import annotations
2+
import datetime
23
import uuid
34
from sqlalchemy import (
45
UUID,
56
Column,
67
Integer,
78
String,
89
DateTime,
9-
Boolean,
1010
ForeignKey,
1111
)
1212
from sqlalchemy.orm import declarative_base
@@ -15,40 +15,29 @@
1515
Base = declarative_base()
1616

1717

18-
class Athlete(Base):
19-
__tablename__ = "athletes"
18+
class User(Base):
19+
__tablename__ = "user"
2020

21-
uuid = Column(UUID, primary_key=True, default=uuid.uuid4)
21+
uuid = Column(UUID, primary_key=True, nullable=False, default=uuid.uuid4)
2222
athlete_id = Column(Integer, unique=True)
23-
resource_state = Column(Integer)
24-
firstname = Column(String)
25-
lastname = Column(String)
26-
profile_medium = Column(String)
27-
profile = Column(String)
28-
city = Column(String)
29-
state = Column(String)
30-
country = Column(String)
31-
sex = Column(String)
32-
premium = Column(Boolean)
33-
summit = Column(Boolean)
34-
created_at = Column(DateTime)
23+
created_at = Column(DateTime, nullable=False, default=datetime.datetime.now)
3524
updated_at = Column(DateTime)
3625

37-
auth = relationship("Auth", uselist=False, back_populates="athlete")
26+
auth = relationship("Auth", uselist=False, back_populates="user")
3827

3928

4029
class Auth(Base):
4130
__tablename__ = "auth"
42-
uuid = Column(UUID, primary_key=True, default=uuid.uuid4)
31+
uuid = Column(UUID, primary_key=True, nullable=False, default=uuid.uuid4)
4332
access_token = Column(String)
4433
refresh_token = Column(String)
4534
expires_at = Column(Integer)
4635
scope = Column(String)
47-
athlete_id = Column(Integer, ForeignKey("athletes.athlete_id"))
48-
athlete = relationship(Athlete.__name__, back_populates="auth")
36+
athlete_id = Column(Integer, ForeignKey("user.athlete_id"))
37+
user = relationship(User.__name__, back_populates="auth")
4938

50-
created_at = Column(DateTime)
51-
updated_at = Column(DateTime)
39+
created_at = Column(DateTime, nullable=False, default=datetime.datetime.now)
40+
updated_at = Column(DateTime, nullable=False, default=datetime.datetime.now)
5241

5342

5443
class ToDictMixin:

src/app/main.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
from dotenv import load_dotenv
3-
from fastapi import FastAPI, Request, HTTPException
3+
from fastapi import FastAPI, Request
44
from fastapi.responses import HTMLResponse
55
from fastapi.staticfiles import StaticFiles
66
from fastapi.templating import Jinja2Templates
@@ -31,17 +31,19 @@
3131

3232
@app.get("/welcome", response_class=HTMLResponse)
3333
async def welcome(request: Request, uuid: str):
34-
db = Database(settings.postgres_connection_string)
34+
db = Database(
35+
settings.postgres_connection_string, encryption_key=settings.encryption_key
36+
)
3537

3638
try:
37-
athlete = db.get_athlete(uuid)
38-
if athlete is None:
39+
user = db.get_user(uuid)
40+
if user is None:
3941
return templates.TemplateResponse(
40-
request, "error.html", {"error": "Athlete not found"}
42+
request, "error.html", {"error": "User not found"}
4143
) # Provide error message
42-
return templates.TemplateResponse(request, "welcome.html", {"athlete": athlete})
44+
return templates.TemplateResponse(request, "welcome.html")
4345
except Exception:
44-
logger.exception(f"Error fetching athlete with UUID {uuid}:")
45-
raise HTTPException(
46-
status_code=500, detail="Failed to retrieve athlete data"
47-
) # Use HTTPException
46+
logger.exception(f"Error fetching User with UUID {uuid}:")
47+
return templates.TemplateResponse(
48+
request, "error.html", {"error": "User not found"}
49+
) # Provide error message

0 commit comments

Comments
 (0)