Add full-stack BTC portfolio web app

Multi-user FastAPI + React app with JWT auth, SQLite storage, and
CoinGecko price integration. Dockerized with docker-compose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-03-23 22:15:40 +01:00
parent 84679639ef
commit 3907414742
27 changed files with 859 additions and 0 deletions
@@ -0,0 +1,67 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel
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
price_eur: float
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).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.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()
+33
View File
@@ -0,0 +1,33 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from ..database import get_db
from .. import models
from ..dependencies import get_current_user
from ..services.btc import get_btc_price_eur
router = APIRouter()
@router.get("/stats")
def get_stats(
db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user),
):
purchases = db.query(models.Purchase).filter(models.Purchase.user_id == current_user.id).all()
total_invested = sum(p.amount_eur for p in purchases)
total_btc = sum(p.amount_eur / p.price_eur for p in purchases) if purchases else 0.0
average_price = total_invested / total_btc if total_btc > 0 else 0.0
current_price = get_btc_price_eur()
portfolio_value = total_btc * current_price
profit_loss = portfolio_value - total_invested
return {
"total_invested": round(total_invested, 2),
"total_btc": round(total_btc, 8),
"average_price": round(average_price, 2),
"current_price": round(current_price, 2),
"portfolio_value": round(portfolio_value, 2),
"profit_loss": round(profit_loss, 2),
}
+42
View File
@@ -0,0 +1,42 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel
from ..database import get_db
from .. import models
from ..auth import hash_password, verify_password, create_access_token
router = APIRouter()
class UserCreate(BaseModel):
username: str
password: str
class Token(BaseModel):
access_token: str
token_type: str
@router.post("/register", status_code=status.HTTP_201_CREATED)
def register(user_in: UserCreate, db: Session = Depends(get_db)):
existing = db.query(models.User).filter(models.User.username == user_in.username).first()
if existing:
raise HTTPException(status_code=400, detail="Username already taken")
user = models.User(
username=user_in.username,
password=hash_password(user_in.password),
)
db.add(user)
db.commit()
return {"message": "User created"}
@router.post("/login", response_model=Token)
def login(user_in: UserCreate, db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.username == user_in.username).first()
if not user or not verify_password(user_in.password, user.password):
raise HTTPException(status_code=401, detail="Invalid credentials")
token = create_access_token({"sub": user.username})
return {"access_token": token, "token_type": "bearer"}