Files
BTC-Portfolio/btc-portfolio/backend/app/routes/purchases.py
T
Jonathan 85455f3271 Security hardening: secrets, validation, Docker, and error handling
- Add root .gitignore to prevent btc_wallet.py (with RPC credentials) from being committed
- Load JWT SECRET_KEY from environment variable instead of hardcoded value
- Restrict CORS to explicit methods/headers instead of wildcards
- Add Pydantic Field validation (gt=0) to purchase amounts and user credentials
- Add logging to all silent exception handlers in btc.py
- Run backend and frontend Docker containers as non-root appuser
- Add .dockerignore for both backend and frontend
- Pass SECRET_KEY env var through docker-compose; add healthchecks to both services
- Update bcrypt from pinned 3.2.2 to >=4.0.0
- Capture error objects in frontend catch blocks; check admin delete response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:40:41 +01:00

100 lines
2.8 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import List
from datetime import datetime
from ..database import get_db
from .. import models
from ..dependencies import get_current_user
router = APIRouter()
class PurchaseCreate(BaseModel):
amount_eur: float = Field(gt=0, le=10_000_000)
price_eur: float = Field(gt=0, le=10_000_000)
class PurchaseUpdate(BaseModel):
amount_eur: float = Field(gt=0, le=10_000_000)
price_eur: float = Field(gt=0, le=10_000_000)
created_at: datetime
class PurchaseOut(BaseModel):
id: int
amount_eur: float
price_eur: float
created_at: datetime
class Config:
from_attributes = True
@router.get("/purchases", response_model=List[PurchaseOut])
def list_purchases(
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
return (
db.query(models.Purchase)
.filter(models.Purchase.user_id == current_user.id)
.order_by(models.Purchase.created_at)
.all()
)
@router.post("/purchases", response_model=PurchaseOut, status_code=status.HTTP_201_CREATED)
def add_purchase(
purchase_in: PurchaseCreate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
purchase = models.Purchase(
amount_eur=purchase_in.amount_eur,
price_eur=purchase_in.price_eur,
user_id=current_user.id,
)
db.add(purchase)
db.commit()
db.refresh(purchase)
return purchase
@router.put("/purchases/{purchase_id}", response_model=PurchaseOut)
def update_purchase(
purchase_id: int,
purchase_in: PurchaseUpdate,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
purchase = db.query(models.Purchase).filter(
models.Purchase.id == purchase_id,
models.Purchase.user_id == current_user.id,
).first()
if not purchase:
raise HTTPException(status_code=404, detail="Purchase not found")
purchase.amount_eur = purchase_in.amount_eur
purchase.price_eur = purchase_in.price_eur
purchase.created_at = purchase_in.created_at
db.commit()
db.refresh(purchase)
return purchase
@router.delete("/purchases/{purchase_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_purchase(
purchase_id: int,
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
purchase = db.query(models.Purchase).filter(
models.Purchase.id == purchase_id,
models.Purchase.user_id == current_user.id,
).first()
if not purchase:
raise HTTPException(status_code=404, detail="Purchase not found")
db.delete(purchase)
db.commit()