feat: initial FastAPI stub with auth, user management, and inventory CRUD

Sets up the complete application skeleton for Flipventory v2, an inventory
management tool for small/medium resellers using chaotic stockpiling.

Architecture
------------
- app/config.py         pydantic-settings; reads .env; holds admin_email guard
- app/database.py       SQLAlchemy 2.x engine, SessionLocal, Base, get_db dep
- app/models/user.py    User (email, bcrypt hash, is_admin bool, is_active)
- app/models/item.py    StorageBin + Item with FOUND/LISTED/SOLD/ARCHIVED enum
- app/schemas/          Pydantic request/response shapes (separate from models)
- app/auth/security.py  bcrypt password hashing + HS256 JWT creation/validation
- app/routers/auth.py   POST /auth/register, POST /auth/login, GET /auth/me
- app/routers/users.py  Admin-only CRUD for user accounts
- app/routers/items.py  /bins and /items CRUD with role-based access control

Key design decisions
--------------------
- martin@hohenberg.jp is ALWAYS admin: enforced on every login, cannot be
  demoted, cannot be deactivated. Prevents accidental lockout.
- All other registered e-mails are customers (is_admin=False).
- Customers can manage their own bins and items; admins see everything.
- Item hard-delete is admin-only; customers archive instead (audit safety).
- SQLite default for zero-infra local dev; any SQLAlchemy URL works in prod.
- Tables are created on startup via create_tables(); swap for Alembic later.

Addresses v1 Gitea issues
--------------------------
- Issue #1 (storage bins): StorageBin model + /bins CRUD endpoints
- Issue #2 (item CRUD): Item model + /items CRUD endpoints
- Issue #3 (audit logging): created_by_id / updated_by_id on every item row

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Hohenberg
2026-06-16 10:40:53 +02:00
parent fe774c924f
commit 4e0d0d3962
19 changed files with 1332 additions and 0 deletions

64
app/database.py Normal file
View File

@@ -0,0 +1,64 @@
"""
Database engine and session factory.
We use SQLAlchemy 2.x with the "declarative" ORM style. All models should
inherit from ``Base`` defined here.
Session lifecycle:
- ``get_db()`` is a FastAPI dependency that opens a session per request and
guarantees it is closed (and rolled back on error) when the request ends.
"""
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, sessionmaker
from app.config import settings
# connect_args is SQLite-specific: it allows the same connection to be used
# from multiple threads, which is required because FastAPI runs in a thread
# pool. This flag is harmless / unused for PostgreSQL.
_connect_args = (
{"check_same_thread": False}
if settings.database_url.startswith("sqlite")
else {}
)
engine = create_engine(settings.database_url, connect_args=_connect_args)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
"""All ORM models inherit from this class."""
pass
def get_db():
"""
FastAPI dependency that yields a database session.
Usage::
@router.get("/things")
def list_things(db: Session = Depends(get_db)):
...
"""
db = SessionLocal()
try:
yield db
finally:
db.close()
def create_tables():
"""Create all tables that are registered on Base.metadata.
Called once at application startup (see main.py). Safe to run multiple
times SQLAlchemy will not recreate existing tables.
"""
# Importing models here ensures they are registered on Base.metadata
# before create_all() is called.
import app.models.user # noqa: F401
import app.models.item # noqa: F401
Base.metadata.create_all(bind=engine)