Initial Claude implementation

This commit is contained in:
Ian Roddis committed 2026-06-22 14:26:44 -03:00
commit e2d3ab9c2a
63 files changed
+3930

No files matched your search

View File
Whitespace-only changes.
+52
View File
@@ -0,0 +1,52 @@
"""Application configuration.
Settings are read from environment variables first, then from a `.env` file.
Environment variables always take precedence over the file.
"""
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
case_sensitive=False,
)
# General
app_name: str = "FastAPI Skeleton"
debug: bool = False
# Security / JWT
secret_key: str = "change-me-please-use-a-long-random-string"
algorithm: str = "HS256"
access_token_expire_minutes: int = 15
refresh_token_expire_days: int = 7
# Database — single URL drives SQLite or Postgres.
# SQLite: sqlite+aiosqlite:///./app.db
# Postgres: postgresql+asyncpg://user:pass@host:5432/dbname
database_url: str = "sqlite+aiosqlite:///./app.db"
auto_create_tables: bool = True
# Web / cookies (the HTMX UI stores the access token in an httpOnly cookie)
cookie_name: str = "access_token"
cookie_secure: bool = False
# CORS
cors_origins: list[str] = ["*"]
# Bootstrap superuser (used by scripts/seed.py)
first_superuser_email: str = "admin@example.com"
first_superuser_password: str = "changethis"
@lru_cache
def get_settings() -> Settings:
return Settings()
settings = get_settings()
+46
View File
@@ -0,0 +1,46 @@
"""Async SQLAlchemy engine, session factory, and declarative base.
Works with SQLite (aiosqlite) or Postgres (asyncpg) depending on DATABASE_URL.
"""
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.orm import DeclarativeBase
from app.core.config import settings
class Base(DeclarativeBase):
"""Declarative base shared by all models."""
engine = create_async_engine(settings.database_url, echo=settings.debug, future=True)
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
"""FastAPI dependency yielding a request-scoped session."""
async with AsyncSessionLocal() as session:
yield session
async def init_db() -> None:
"""Create all tables. Convenient for dev/SQLite and tests.
In production prefer Alembic migrations (``alembic upgrade head``).
"""
# Import models so they register with Base.metadata before create_all.
import app.models # noqa: F401
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
+72
View File
@@ -0,0 +1,72 @@
"""Password hashing and JWT token helpers."""
import hashlib
import uuid
from datetime import UTC, datetime, timedelta
import bcrypt
import jwt
from app.core.config import settings
ACCESS_TOKEN_TYPE = "access"
REFRESH_TOKEN_TYPE = "refresh"
# --- Passwords ---------------------------------------------------------------
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def verify_password(password: str, hashed: str) -> bool:
try:
return bcrypt.checkpw(password.encode(), hashed.encode())
except ValueError:
return False
# --- JWT ---------------------------------------------------------------------
def _create_token(
subject: str | int,
expires_delta: timedelta,
token_type: str,
) -> tuple[str, dict]:
now = datetime.now(UTC)
payload: dict = {
"sub": str(subject),
"iat": now,
"exp": now + expires_delta,
"type": token_type,
"jti": uuid.uuid4().hex,
}
token = jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm)
return token, payload
def create_access_token(subject: str | int) -> str:
token, _ = _create_token(
subject,
timedelta(minutes=settings.access_token_expire_minutes),
ACCESS_TOKEN_TYPE,
)
return token
def create_refresh_token(subject: str | int) -> tuple[str, dict]:
"""Return ``(token, payload)`` so the caller can persist jti/exp for revocation."""
return _create_token(
subject,
timedelta(days=settings.refresh_token_expire_days),
REFRESH_TOKEN_TYPE,
)
def decode_token(token: str) -> dict:
"""Decode and validate a JWT. Raises ``jwt.PyJWTError`` on failure."""
return jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
def hash_token(token: str) -> str:
"""Stable hash used to store refresh tokens without keeping the raw value."""
return hashlib.sha256(token.encode()).hexdigest()