feat(backend): FastAPI + libSQL + auth register/login/me + tests (TDD)

- FastAPI app with lifespan, CORS, health endpoint
- SQLAlchemy 2.0 async with aiosqlite, Base/UUIDMixin/TenantMixin/TimestampMixin
- Tenant and User models with multi-tenant isolation
- Auth: register (creates tenant+user), login, /me endpoint
- JWT HS256 tokens, bcrypt password hashing
- Alembic async setup with initial migration
- 6 passing tests (register, login, wrong password, me, no token, health)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:26:31 +02:00
parent c3482bba8d
commit 907b7be0fd
24 changed files with 623 additions and 0 deletions

View File

32
backend/app/db/base.py Normal file
View File

@@ -0,0 +1,32 @@
import uuid
from datetime import datetime, UTC
from sqlalchemy import String, Text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
def uuid7() -> str:
return str(uuid.uuid4())
class Base(DeclarativeBase):
pass
class UUIDMixin:
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=uuid7)
class TenantMixin:
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
class TimestampMixin:
created_at: Mapped[str] = mapped_column(
Text, default=lambda: datetime.now(UTC).isoformat()
)
updated_at: Mapped[str] = mapped_column(
Text,
default=lambda: datetime.now(UTC).isoformat(),
onupdate=lambda: datetime.now(UTC).isoformat(),
)

View File

@@ -0,0 +1,4 @@
from app.db.models.tenant import Tenant
from app.db.models.user import User
__all__ = ["Tenant", "User"]

View File

@@ -0,0 +1,18 @@
from sqlalchemy import String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, UUIDMixin, TimestampMixin
class Tenant(Base, UUIDMixin, TimestampMixin):
__tablename__ = "tenants"
nume: Mapped[str] = mapped_column(String(200))
cui: Mapped[str | None] = mapped_column(String(20))
reg_com: Mapped[str | None] = mapped_column(String(30))
adresa: Mapped[str | None] = mapped_column(Text)
telefon: Mapped[str | None] = mapped_column(String(20))
email: Mapped[str | None] = mapped_column(String(200))
iban: Mapped[str | None] = mapped_column(String(34))
banca: Mapped[str | None] = mapped_column(String(100))
plan: Mapped[str] = mapped_column(String(20), default="trial")
trial_expires_at: Mapped[str | None] = mapped_column(Text)

View File

@@ -0,0 +1,13 @@
from sqlalchemy import Boolean, String
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base, UUIDMixin, TenantMixin, TimestampMixin
class User(Base, UUIDMixin, TenantMixin, TimestampMixin):
__tablename__ = "users"
email: Mapped[str] = mapped_column(String(200), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(String(200))
nume: Mapped[str] = mapped_column(String(200))
rol: Mapped[str] = mapped_column(String(20), default="owner")
activ: Mapped[bool] = mapped_column(Boolean, default=True)

11
backend/app/db/session.py Normal file
View File

@@ -0,0 +1,11 @@
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.config import settings
engine = create_async_engine(settings.DATABASE_URL, echo=False)
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_db():
async with AsyncSessionLocal() as session:
yield session