Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
.venv/
apps/ia/.env
apps/backend/venv/
apps/backend/.env
apps/backend/*.db
apps/backend/__pycache__/
apps/backend/src/__pycache__/
apps/backend/src/**/__pycache__/
apps/backend/tests/__pycache__/
apps/backend/.pytest_cache/
node_modules/
apps/web/node_modules/
apps/web/.next/
90 changes: 90 additions & 0 deletions apps/backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# La Vida Luca Backend API

FastAPI backend for the La Vida Luca application providing user authentication and activity management.

## Features

- **User Authentication**: JWT-based authentication with registration and login
- **Activity Management**: CRUD operations for user activities
- **Database Integration**: SQLAlchemy with SQLite (configurable)
- **API Documentation**: Automatic Swagger/OpenAPI documentation
- **CORS Support**: Configurable CORS for frontend integration

## Quick Start

1. **Start the server**:
```bash
./start.sh
```

2. **Manual setup** (if needed):
```bash
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
uvicorn src.main:app --reload
```

3. **Access the API**:
- API: http://localhost:8000
- Documentation: http://localhost:8000/docs
- Health check: http://localhost:8000/health

## API Endpoints

### Authentication
- `POST /auth/register` - Register new user
- `POST /auth/login` - Login user (returns JWT token)

### Activities (requires authentication)
- `GET /activities/` - Get user's activities
- `POST /activities/` - Create new activity
- `GET /activities/{id}` - Get specific activity

## Testing

Run the test suite:
```bash
source venv/bin/activate
python -m pytest tests/ -v
```

## Configuration

Environment variables (create `.env` file):
```
SECRET_KEY=your-secret-key-here
DATABASE_URL=sqlite:///./lavidaluca.db
ALLOWED_ORIGINS=["http://localhost:3000"]
ACCESS_TOKEN_EXPIRE_MINUTES=30
```

## Project Structure

```
apps/backend/
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ main.py # FastAPI application
β”‚ β”œβ”€β”€ config.py # Configuration settings
β”‚ β”œβ”€β”€ database.py # Database configuration
β”‚ β”œβ”€β”€ models.py # SQLAlchemy models
β”‚ β”œβ”€β”€ schemas.py # Pydantic schemas
β”‚ β”œβ”€β”€ activities.py # Activities router
β”‚ └── auth/
β”‚ β”œβ”€β”€ router.py # Authentication router
β”‚ └── utils.py # Auth utilities (JWT, passwords)
β”œβ”€β”€ tests/
β”‚ β”œβ”€β”€ test_auth.py # Authentication tests
β”‚ └── test_api.py # API endpoint tests
β”œβ”€β”€ requirements.txt # Python dependencies
└── start.sh # Startup script
```

## Dependencies

- **FastAPI**: Modern web framework for APIs
- **SQLAlchemy**: Database ORM
- **Passlib**: Password hashing
- **python-jose**: JWT token handling
- **Uvicorn**: ASGI server
- **Pytest**: Testing framework
12 changes: 12 additions & 0 deletions apps/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy==2.0.23
python-jose[cryptography]==3.3.0
python-multipart==0.0.6
passlib[bcrypt]==1.7.4
python-dotenv==1.0.0
pydantic-settings==2.0.3
email-validator==2.1.0
pytest==7.4.3
pytest-asyncio==0.21.1
httpx==0.25.2
1 change: 1 addition & 0 deletions apps/backend/src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# La Vida Luca Backend API
52 changes: 52 additions & 0 deletions apps/backend/src/activities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List

try:
from .database import get_db
from .models import Activity, User
from .schemas import ActivityCreate, ActivityResponse
from .auth.router import get_current_user
except ImportError:
from database import get_db
from models import Activity, User
from schemas import ActivityCreate, ActivityResponse
from auth.router import get_current_user

router = APIRouter(prefix="/activities", tags=["activities"])


@router.post("/", response_model=ActivityResponse)
def create_activity(
activity: ActivityCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
db_activity = Activity(**activity.model_dump(), user_id=current_user.id)
db.add(db_activity)
db.commit()
db.refresh(db_activity)
return db_activity


@router.get("/", response_model=List[ActivityResponse])
def get_activities(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
skip: int = 0,
limit: int = 100
):
activities = db.query(Activity).filter(Activity.user_id == current_user.id).offset(skip).limit(limit).all()
return activities


@router.get("/{activity_id}", response_model=ActivityResponse)
def get_activity(
activity_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
activity = db.query(Activity).filter(Activity.id == activity_id, Activity.user_id == current_user.id).first()
if activity is None:
raise HTTPException(status_code=404, detail="Activity not found")
return activity
1 change: 1 addition & 0 deletions apps/backend/src/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Authentication module
76 changes: 76 additions & 0 deletions apps/backend/src/auth/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from jose import JWTError, jwt

try:
from ..database import get_db
from ..models import User
from ..schemas import UserCreate, UserResponse, Token, TokenData
from .utils import verify_password, get_password_hash, create_access_token
from ..config import settings
except ImportError:
from database import get_db
from models import User
from schemas import UserCreate, UserResponse, Token, TokenData
from auth.utils import verify_password, get_password_hash, create_access_token
from config import settings

router = APIRouter(prefix="/auth", tags=["authentication"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")


def authenticate_user(db: Session, email: str, password: str):
user = db.query(User).filter(User.email == email).first()
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user


def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = TokenData(email=email)
except JWTError:
raise credentials_exception
user = db.query(User).filter(User.email == token_data.email).first()
if user is None:
raise credentials_exception
return user


@router.post("/register", response_model=UserResponse)
def register(user: UserCreate, db: Session = Depends(get_db)):
db_user = db.query(User).filter(User.email == user.email).first()
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")

hashed_password = get_password_hash(user.password)
db_user = User(email=user.email, hashed_password=hashed_password, full_name=user.full_name)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user


@router.post("/login", response_model=Token)
def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = authenticate_user(db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(data={"sub": user.email})
return {"access_token": access_token, "token_type": "bearer"}
25 changes: 25 additions & 0 deletions apps/backend/src/auth/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
from passlib.context import CryptContext

try:
from ..config import settings
except ImportError:
from config import settings

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)


def verify_password(plain_password: str, hashed_password: str):
return pwd_context.verify(plain_password, hashed_password)


def get_password_hash(password: str):
return pwd_context.hash(password)
16 changes: 16 additions & 0 deletions apps/backend/src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from pydantic_settings import BaseSettings
from typing import List


class Settings(BaseSettings):
SECRET_KEY: str = "your-secret-key-here-change-in-production"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
DATABASE_URL: str = "sqlite:///./lavidaluca.db"
ALLOWED_ORIGINS: List[str] = ["http://localhost:3000", "http://localhost:8000"]

class Config:
env_file = ".env"


settings = Settings()
23 changes: 23 additions & 0 deletions apps/backend/src/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

try:
from .config import settings
except ImportError:
from config import settings

SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL

engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} if "sqlite" in SQLALCHEMY_DATABASE_URL else {}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
43 changes: 43 additions & 0 deletions apps/backend/src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

try:
# Try relative imports first
from .auth.router import router as auth_router
from .activities import router as activities_router
from .config import settings
from .database import engine
from .models import Base
except ImportError:
# Fallback to absolute imports for testing
from auth.router import router as auth_router
from activities import router as activities_router
from config import settings
from database import engine
from models import Base

# Create database tables
Base.metadata.create_all(bind=engine)

app = FastAPI(title="La Vida Luca API")

app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(auth_router)
app.include_router(activities_router)


@app.get("/")
def read_root():
return {"message": "La Vida Luca API is running"}


@app.get("/health")
def health_check():
return {"status": "healthy"}
Loading