Tracker de vie auto-hébergé : suivi poids/calories/sport avec planning de pesées, sevrage tabac (vape) avec modèle de coût DIY et économies, et finances personnelles avec import de relevés bancaires. Architecture : FastAPI + SQLAlchemy 2.0 + PostgreSQL 16, React 18 + TS + Vite + Tailwind + ECharts, déploiement Docker Compose. Modules auto-découverts des deux côtés (pkgutil / import.meta.glob) et framework de connecteurs à deux voies (importeurs de fichiers + ingestion JSON) pour brancher de nouvelles sources sans toucher au noyau. Validé : 292 tests pytest, tsc + vite build, contrat API/web vérifié contre le schéma OpenAPI, et déploiement Docker réel sur PostgreSQL 16 (28 tables, SPA servie par nginx, wizard de premier démarrage). Documentation : README.md, docs/GUIDE.md, CONVENTIONS.md, et les documents de conception et de recherche dans docs/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
37 lines
1.4 KiB
Python
37 lines
1.4 KiB
Python
from datetime import datetime
|
||
|
||
from sqlalchemy import DateTime, ForeignKey, String
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from app.core.database import Base
|
||
from app.core.mixins import JSONB_V, TimestampMixin
|
||
|
||
|
||
class User(TimestampMixin, Base):
|
||
__tablename__ = "users"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
||
password_hash: Mapped[str] = mapped_column(String(255))
|
||
display_name: Mapped[str] = mapped_column(String(100))
|
||
is_active: Mapped[bool] = mapped_column(default=True)
|
||
|
||
|
||
class DeviceApiKey(TimestampMixin, Base):
|
||
"""Long-lived scoped token for device bridges (Android Health Connect…)."""
|
||
|
||
__tablename__ = "device_api_keys"
|
||
|
||
id: Mapped[int] = mapped_column(primary_key=True)
|
||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), index=True)
|
||
name: Mapped[str] = mapped_column(
|
||
String(100)
|
||
) # ex: "Pixel 8 – pont Health Connect"
|
||
key_prefix: Mapped[str] = mapped_column(String(12), unique=True, index=True)
|
||
key_hash: Mapped[str] = mapped_column(String(64)) # sha256 hex of the full key
|
||
scopes: Mapped[list[str]] = mapped_column(
|
||
JSONB_V, default=list
|
||
) # ["ingest:health"]
|
||
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|