Skip to content
Open
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
29 changes: 29 additions & 0 deletions assets/backend/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
from datetime import datetime, timedelta
from typing import Optional

from jose import JWTError, jwt
from passlib.context import CryptContext

# In production, use environment variables for the secret key
SECRET_KEY = "your-very-secret-key-for-dashboard-development"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

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

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

def get_password_hash(password):
return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
19 changes: 19 additions & 0 deletions assets/backend/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./dashboard.db"

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

Base = declarative_base()

def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
109 changes: 109 additions & 0 deletions assets/backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from typing import List

import models
import schemas
import auth
from database import SessionLocal, engine, get_db
from jose import JWTError, jwt

models.Base.metadata.create_all(bind=engine)

app = FastAPI(title="Python Dashboard API")

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

def get_user_by_email(db: Session, email: str):
return db.query(models.User).filter(models.User.email == email).first()

async 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, auth.SECRET_KEY, algorithms=[auth.ALGORITHM])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
token_data = schemas.TokenData(email=email)
except JWTError:
raise credentials_exception
user = get_user_by_email(db, email=token_data.email)
if user is None:
raise credentials_exception
return user

# --- Auth Endpoints ---

@app.post("/register", response_model=schemas.User)
def register_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
db_user = get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
hashed_password = auth.get_password_hash(user.password)
new_user = models.User(email=user.email, hashed_password=hashed_password)
db.add(new_user)
db.commit()
db.refresh(new_user)
return new_user

@app.post("/token", response_model=schemas.Token)
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
user = get_user_by_email(db, email=form_data.username)
if not user or not auth.verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = auth.timedelta(minutes=auth.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = auth.create_access_token(
data={"sub": user.email}, expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}

# --- Dashboard CRUD Endpoints ---

@app.post("/items/", response_model=schemas.Item)
def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)):
db_item = models.Item(**item.model_dump(), owner_id=current_user.id)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item

@app.get("/items/", response_model=List[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)):
items = db.query(models.Item).filter(models.Item.owner_id == current_user.id).offset(skip).limit(limit).all()
return items

@app.put("/items/{item_id}", response_model=schemas.Item)
def update_item(item_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)):
db_item = db.query(models.Item).filter(models.Item.id == item_id, models.Item.owner_id == current_user.id).first()
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")

for key, value in item.model_dump().items():
setattr(db_item, key, value)

db.commit()
db.refresh(db_item)
return db_item

@app.delete("/items/{item_id}")
def delete_item(item_id: int, db: Session = Depends(get_db), current_user: models.User = Depends(get_current_user)):
db_item = db.query(models.Item).filter(models.Item.id == item_id, models.Item.owner_id == current_user.id).first()
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")

db.delete(db_item)
db.commit()
return {"detail": "Item deleted"}

@app.get("/users/me/", response_model=schemas.User)
def read_users_me(current_user: models.User = Depends(get_current_user)):
return current_user
23 changes: 23 additions & 0 deletions assets/backend/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from sqlalchemy import Column, Integer, String, Float, ForeignKey
from sqlalchemy.orm import relationship
from database import Base

class User(Base):
__tablename__ = "users"

id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)

items = relationship("Item", back_populates="owner")

class Item(Base):
__tablename__ = "items"

id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(String, index=True)
value = Column(Float, default=0.0)
owner_id = Column(Integer, ForeignKey("users.id"))

owner = relationship("User", back_populates="items")
7 changes: 7 additions & 0 deletions assets/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fastapi
uvicorn
sqlalchemy
pydantic
python-jose[cryptography]
passlib[bcrypt]
python-multipart
37 changes: 37 additions & 0 deletions assets/backend/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from pydantic import BaseModel
from typing import List, Optional

class ItemBase(BaseModel):
title: str
description: Optional[str] = None
value: float = 0.0

class ItemCreate(ItemBase):
pass

class Item(ItemBase):
id: int
owner_id: int

class Config:
from_attributes = True

class UserBase(BaseModel):
email: str

class UserCreate(UserBase):
password: str

class User(UserBase):
id: int
items: List[Item] = []

class Config:
from_attributes = True

class Token(BaseModel):
access_token: str
token_type: str

class TokenData(BaseModel):
email: Optional[str] = None
51 changes: 51 additions & 0 deletions dashboard-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Building a Python Dashboard with Auth and CRUD

When building a dashboard environment in Python that requires **User Authentication**, **CRUD functionality** (Create, Read, Update, Delete), and a **responsive UI**, selecting the right architecture is critical.

Here is the blueprint for the most efficient and scalable Python dashboard architectures.

## Architecture 1: The "Batteries-Included" Approach (Django)
*Best for getting to market quickly with robust security and built-in features.*

- **Backend:** Django
- **Database:** PostgreSQL (or SQLite for dev)
- **Frontend:** HTML templates + HTMX + Tailwind CSS
- **Authentication:** Built-in Django Auth (Session-based)

**Key Features:**
1. **Built-in Admin Panel:** Automatically generates a dashboard to manage users and CRUD data.
2. **Security:** Out-of-the-box protection against CSRF, XSS, and SQL Injection.
3. **Responsive UI:** Using Tailwind CSS via CDN or a build step makes it fully responsive.
4. **Dynamic Data:** HTMX allows you to update charts and data tables dynamically without reloading the page, mimicking a Single Page App (SPA) without writing JavaScript.

## Architecture 2: The Modern API-Driven Approach (FastAPI)
*Best for data-heavy dashboards, microservices, or pairing with a dedicated frontend team.*

- **Backend:** FastAPI
- **Database:** SQLAlchemy (ORM) + Alembic (Migrations)
- **Frontend:** React.js / Next.js / Vue.js
- **Authentication:** `FastAPI-Users` (JWT or Cookie-based)

**Key Features:**
1. **High Performance:** FastAPI is built on ASGI, making it perfect for asynchronous data fetching (e.g., live dashboard metrics).
2. **Auto-Generated Docs:** Provides Swagger UI out-of-the-box to easily test your CRUD API endpoints.
3. **Decoupled:** The frontend and backend can be hosted and scaled independently.

## Architecture 3: The Pure Python Approach (Reflex)
*Best for data scientists and developers who want to write 100% Python.*

- **Framework:** Reflex (formerly Pynecone)
- **Database:** Built-in SQLModel support
- **Frontend:** Compiled to React under the hood (you write pure Python)

**Key Features:**
1. **Zero JavaScript:** Build complex, responsive, interactive UIs without touching JS or HTML.
2. **State Management:** Handles user state, authentication flows, and data updates natively in Python.
3. **Component Library:** Comes with a rich set of responsive UI components (buttons, graphs, data tables).

## Essential Features to Implement
Regardless of the framework chosen, a modern dashboard should include:
- **Role-Based Access Control (RBAC):** Differentiating between 'Admin', 'Editor', and 'Viewer' permissions.
- **Data Visualization:** Integration with libraries like Plotly, Bokeh, or Chart.js for rendering metrics.
- **Responsive Layout:** A sidebar navigation (collapsible on mobile) and a grid-based data layout.
- **Audit Logging:** Tracking which user performed CRUD operations.