Phase 2.1 : preparation au deploiement public

Rend une instance exposee sur Internet exploitable sans la transformer en
infrastructure : le produit reste local-first, sans compte et sans telemetrie.

Protection du service : limitation de debit en memoire par client et par
famille d'endpoint (health et ready jamais limites), interrupteur
SHARING_ENABLED, capacite maximale de partages, taille propre aux partages.
X-Forwarded-For n'est cru que si TRUST_PROXY_HEADERS l'autorise, et on lit alors
la derniere valeur, celle ajoutee par le proxy.

Expiration facultative des partages (24 h a 90 jours, ou jamais) : un lien
expire repond 410 et la page publique le dit, sans autre detail. Le proprietaire
peut prolonger ; aucune purge automatique.

Protection des liens non repertories : robots.txt, X-Robots-Tag et meta robots
sur /s/ et /api/, Referrer-Policy no-referrer sur tout le site, redaction des
jetons dans les journaux d'acces, share_token porte a 128 bits (les anciens
restent valides).

Schema gere par Alembic, applique au demarrage. Une base creee avant Alembic est
estampillee puis migree : verifie sur la base reelle de developpement, 4
partages inchanges et expires_at NULL.

Administration par CLI locale au conteneur (stats, liste, suppression, purge,
sauvegarde a chaud via l'API SQLite, restauration prudente) : aucune interface
/admin exposee.

Le jeton de gestion peut enfin etre exporte dans un fichier .openmind-share puis
reimporte depuis un autre navigateur, apres verification aupres du serveur. Il
n'est toujours jamais ajoute a un export .mmap.

Ajoute une page /privacy dont le texte est centralise, et corrige un bug trouve
en observant les logs : Alembic, lance dans le processus applicatif,
reappliquait la configuration de logs d'alembic.ini et desactivait les loggers
existants, ce qui faisait disparaitre les journaux d'acces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 09:22:17 +02:00
parent e3c530348e
commit 49870dcd56
51 changed files with 3341 additions and 110 deletions

View File

@@ -15,24 +15,60 @@ os.environ["STATIC_DIR"] = f"{_TMP_DIR}/static"
os.environ["APP_NAME"] = "OpenMind"
os.environ["MAX_NODES"] = "5000"
os.environ["MAX_MAP_SIZE_MB"] = "10"
# Les limites de debit sont desactivees par defaut dans les tests : chaque test
# qui les concerne les active explicitement via la fixture `configure`.
os.environ["SHARE_CREATE_RATE_LIMIT"] = "0"
os.environ["SHARE_READ_RATE_LIMIT"] = "0"
os.environ["SHARE_WRITE_RATE_LIMIT"] = "0"
from fastapi.testclient import TestClient # noqa: E402
from sqlalchemy import text # noqa: E402
from app.config import get_settings # noqa: E402
from app.db.base import Base # noqa: E402
from app.db.migrations import run_migrations # noqa: E402
from app.db.session import engine # noqa: E402
from app.main import app # noqa: E402
from app.services.rate_limit import limiter # noqa: E402
@pytest.fixture(autouse=True)
def _fresh_database() -> Iterator[None]:
"""Base neuve avant chaque test, construite par les vraies migrations.
On passe deliberement par Alembic plutot que par `create_all` : le schema
teste est exactement celui qu'obtiendra une installation reelle.
"""
get_settings.cache_clear()
Base.metadata.drop_all(bind=engine)
Base.metadata.create_all(bind=engine)
with engine.begin() as connection:
connection.execute(text("DROP TABLE IF EXISTS alembic_version"))
run_migrations(engine)
# Les compteurs de debit vivent dans le processus : sans cette remise a
# zero, les tests se limiteraient les uns les autres.
limiter.reset()
yield
get_settings.cache_clear()
@pytest.fixture()
def configure(monkeypatch: pytest.MonkeyPatch): # type: ignore[no-untyped-def]
"""Surcharge des reglages le temps d'un test.
Les valeurs sont posees dans l'environnement puis le cache de `get_settings`
est vide, exactement comme au demarrage de l'application.
"""
def _apply(**values: object) -> None:
for key, value in values.items():
monkeypatch.setenv(key.upper(), str(value))
get_settings.cache_clear()
yield _apply
get_settings.cache_clear()
@pytest.fixture()
def client() -> Iterator[TestClient]:
with TestClient(app) as test_client: