Initial project scaffolding for health tracker app

Set up backend and frontend structure for a health and fitness tracker using Python (FastAPI, SQLModel, DSPy) and React. Includes Docker and Compose configs, authentication, nutrition AI module, health/nutrition/user endpoints, database models, and basic frontend with routing and context. Enables tracking nutrition, health metrics, and user management, with architecture ready for future mobile and cloud deployment.
This commit is contained in:
Carlos Escalante
2026-01-18 10:29:44 -06:00
parent b11e2740ea
commit 5dc6dc88f7
55 changed files with 5751 additions and 0 deletions

0
backend/app/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,25 @@
import dspy
from pydantic import BaseModel, Field
class NutritionalInfo(BaseModel):
name: str
calories: float
protein: float
carbs: float
fats: float
micros: dict | None = None
class ExtractNutrition(dspy.Signature):
"""Extract nutritional information from a food description."""
description: str = dspy.InputField(desc="Description of the food or meal")
nutritional_info: NutritionalInfo = dspy.OutputField(desc="Nutritional information as a structured object")
class NutritionModule(dspy.Module):
def __init__(self):
super().__init__()
self.extract = dspy.ChainOfThought(ExtractNutrition)
def forward(self, description: str):
return self.extract(description=description)
nutrition_module = NutritionModule()

View File

43
backend/app/api/deps.py Normal file
View File

@@ -0,0 +1,43 @@
from typing import Generator
from sqlmodel import Session
from app.db import engine
from typing import Generator, Annotated
from sqlmodel import Session, select
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from pydantic import ValidationError
from app.db import engine
from app.models.user import User
from app.core import security
from app.config import settings
from app.schemas.token import TokenPayload
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/login/access-token")
def get_session() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_session)]
TokenDep = Annotated[str, Depends(oauth2_scheme)]
def get_current_user(session: SessionDep, token: TokenDep) -> User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
user = session.get(User, int(token_data.sub))
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
CurrentUser = Annotated[User, Depends(get_current_user)]

View File

View File

@@ -0,0 +1,8 @@
from fastapi import APIRouter
from app.api.v1.endpoints import users, login, nutrition, health
api_router = APIRouter()
api_router.include_router(login.router, tags=["login"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(nutrition.router, prefix="/nutrition", tags=["nutrition"])
api_router.include_router(health.router, prefix="/health", tags=["health"])

View File

View File

@@ -0,0 +1,35 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from app.api import deps
from app.models.health import HealthMetric
from pydantic import BaseModel
router = APIRouter()
class HealthMetricCreate(BaseModel):
metric_type: str
value: float
unit: str
user_id: int # TODO: remove when auth is fully integrated
@router.post("/", response_model=HealthMetric)
def create_metric(
*,
session: Session = Depends(deps.get_session),
metric_in: HealthMetricCreate,
) -> Any:
metric = HealthMetric(metric_type=metric_in.metric_type, value=metric_in.value, unit=metric_in.unit, user_id=metric_in.user_id)
session.add(metric)
session.commit()
session.refresh(metric)
return metric
@router.get("/{user_id}", response_model=List[HealthMetric])
def read_metrics(
user_id: int,
session: Session = Depends(deps.get_session),
) -> Any:
statement = select(HealthMetric).where(HealthMetric.user_id == user_id)
metrics = session.exec(statement).all()
return metrics

View File

@@ -0,0 +1,35 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlmodel import Session, select
from app.api import deps
from app.core import security
from app.config import settings
from app.models.user import User
from app.schemas.token import Token
router = APIRouter()
@router.post("/login/access-token", response_model=Token)
def login_access_token(
session: Session = Depends(deps.get_session),
form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests
"""
statement = select(User).where(User.email == form_data.username)
user = session.exec(statement).first()
if not user or not security.verify_password(form_data.password, user.password_hash):
raise HTTPException(status_code=400, detail="Incorrect email or password")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": security.create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}

View File

@@ -0,0 +1,54 @@
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlmodel import Session
from app.api import deps
from app.ai.nutrition import nutrition_module, NutritionalInfo
from app.core.security import create_access_token # Just ensuring we have auth imports if needed later
from app.models.user import User
router = APIRouter()
class AnalyzeRequest(BaseModel):
description: str
from app.models.food import FoodLog, FoodItem
from app.api.deps import get_session
from app.core.security import get_password_hash # Not needed
from app.config import settings
@router.post("/analyze", response_model=NutritionalInfo)
def analyze_food(
request: AnalyzeRequest,
) -> Any:
"""
Analyze food description and return nutritional info using DSPy.
"""
try:
result = nutrition_module(description=request.description)
return result.nutritional_info
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/log", response_model=FoodLog)
def log_food(
*,
session: Session = Depends(deps.get_session),
nutrition_info: NutritionalInfo,
current_user: deps.CurrentUser,
) -> Any:
"""
Save food log to database.
"""
food_log = FoodLog(
user_id=current_user.id,
name=nutrition_info.name,
calories=nutrition_info.calories,
protein=nutrition_info.protein,
carbs=nutrition_info.carbs,
fats=nutrition_info.fats,
)
session.add(food_log)
session.commit()
session.refresh(food_log)
return food_log

View File

@@ -0,0 +1,36 @@
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from app.api import deps
from app.core import security
from app.models.user import User
from app.schemas.user import UserCreate, UserRead
router = APIRouter()
@router.post("/", response_model=UserRead)
def create_user(
*,
session: Session = Depends(deps.get_session),
user_in: UserCreate,
) -> Any:
"""
Create new user.
"""
user = session.exec(select(User).where(User.email == user_in.email)).first()
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system",
)
user = User(
email=user_in.email,
username=user_in.username,
password_hash=security.get_password_hash(user_in.password),
)
session.add(user)
session.commit()
session.refresh(user)
return user

13
backend/app/config.py Normal file
View File

@@ -0,0 +1,13 @@
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
DATABASE_URL: str
OPENAI_API_KEY: str | None = None
SECRET_KEY: str = "changethis"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days
class Config:
env_file = ".env"
settings = Settings()

View File

View File

@@ -0,0 +1,7 @@
import dspy
from app.config import settings
def configure_dspy():
if settings.OPENAI_API_KEY:
lm = dspy.LM("openai/gpt-4o-mini", api_key=settings.OPENAI_API_KEY)
dspy.configure(lm=lm)

View File

@@ -0,0 +1,24 @@
from datetime import datetime, timedelta
from typing import Any, Union
from jose import jwt
from passlib.context import CryptContext
from app.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
def create_access_token(subject: Union[str, Any], expires_delta: timedelta = None) -> str:
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)

14
backend/app/db.py Normal file
View File

@@ -0,0 +1,14 @@
from sqlmodel import SQLModel, create_engine, Session, text
from app.config import settings
engine = create_engine(settings.DATABASE_URL)
def get_session():
with Session(engine) as session:
yield session
def init_db():
with Session(engine) as session:
session.exec(text("CREATE EXTENSION IF NOT EXISTS vector"))
session.commit()
SQLModel.metadata.create_all(engine)

33
backend/app/main.py Normal file
View File

@@ -0,0 +1,33 @@
from contextlib import asynccontextmanager
from dotenv import load_dotenv
load_dotenv()
from fastapi import FastAPI
from app.api.v1.api import api_router
from app.db import init_db
from app.core.ai_config import configure_dspy
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
configure_dspy()
yield
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI(title="Healthy Fit API", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://localhost:5174", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router, prefix="/api/v1")
@app.get("/")
def read_root():
return {"message": "Welcome to Healthy Fit API"}

View File

View File

@@ -0,0 +1,27 @@
from datetime import datetime
from typing import Optional, List, Dict
from sqlmodel import Field, SQLModel, JSON
from pgvector.sqlalchemy import Vector
from sqlalchemy import Column
class FoodItem(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=True)
calories: float
protein: float
carbs: float
fats: float
micros: Dict = Field(default={}, sa_column=Column(JSON))
embedding: List[float] = Field(sa_column=Column(Vector(1536))) # OpenAI embedding size
class FoodLog(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
food_item_id: Optional[int] = Field(default=None, foreign_key="fooditem.id")
name: str # In case no food item is linked or custom entry
calories: float
protein: float
carbs: float
fats: float
image_url: Optional[str] = None
timestamp: datetime = Field(default_factory=datetime.utcnow)

View File

@@ -0,0 +1,19 @@
from datetime import datetime
from typing import Optional
from sqlmodel import Field, SQLModel
class HealthMetric(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
metric_type: str = Field(index=True) # e.g., "weight", "cholesterol", "testosterone"
value: float
unit: str
timestamp: datetime = Field(default_factory=datetime.utcnow)
class HealthGoal(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id")
goal_type: str # e.g., "lose_weight", "gain_muscle"
target_value: float
target_date: Optional[datetime] = None
created_at: datetime = Field(default_factory=datetime.utcnow)

View File

@@ -0,0 +1,10 @@
from datetime import datetime
from typing import Optional
from sqlmodel import Field, SQLModel
class User(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
username: str = Field(index=True, unique=True)
email: str = Field(index=True, unique=True)
password_hash: str
created_at: datetime = Field(default_factory=datetime.utcnow)

View File

View File

@@ -0,0 +1,9 @@
from typing import Optional
from sqlmodel import SQLModel
class Token(SQLModel):
access_token: str
token_type: str
class TokenPayload(SQLModel):
sub: Optional[str] = None

View File

@@ -0,0 +1,17 @@
from typing import Optional
from sqlmodel import SQLModel, Field
class UserBase(SQLModel):
email: str
username: str
class UserCreate(UserBase):
password: str
class UserRead(UserBase):
id: int
class UserUpdate(SQLModel):
email: Optional[str] = None
username: Optional[str] = None
password: Optional[str] = None