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:
@@ -4,10 +4,19 @@ from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Response, status
|
||||
|
||||
from app.api.deps import DbSession, ManagementToken
|
||||
from app.api.deps import (
|
||||
DbSession,
|
||||
ManagementToken,
|
||||
RateLimitCreate,
|
||||
RateLimitRead,
|
||||
RateLimitWrite,
|
||||
SharingEnabled,
|
||||
)
|
||||
from app.schemas.share import (
|
||||
ShareCreatedOut,
|
||||
ShareCreateIn,
|
||||
ShareExpiryIn,
|
||||
ShareOwnerOut,
|
||||
SharePublicOut,
|
||||
ShareUpdatedOut,
|
||||
)
|
||||
@@ -26,9 +35,21 @@ def _share_path(share_token: str) -> str:
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Publier un snapshot de carte et obtenir un lien non repertorie",
|
||||
)
|
||||
def create_share(payload: ShareCreateIn, session: DbSession) -> ShareCreatedOut:
|
||||
def create_share(
|
||||
payload: ShareCreateIn,
|
||||
session: DbSession,
|
||||
_sharing: SharingEnabled,
|
||||
_rate: RateLimitCreate,
|
||||
) -> ShareCreatedOut:
|
||||
# `permanent` distingue "aucune preference" (defaut de l'instance) de
|
||||
# "explicitement sans expiration", que le JSON seul ne permet pas de separer.
|
||||
explicit = payload.permanent or payload.expires_in_days is not None
|
||||
share, management_token = share_service.create_share(
|
||||
session, title=payload.title, document=payload.map
|
||||
session,
|
||||
title=payload.title,
|
||||
document=payload.map,
|
||||
expires_in_days=None if payload.permanent else payload.expires_in_days,
|
||||
expiry_explicit=explicit,
|
||||
)
|
||||
return ShareCreatedOut(
|
||||
share_token=share.share_token,
|
||||
@@ -37,6 +58,7 @@ def create_share(payload: ShareCreateIn, session: DbSession) -> ShareCreatedOut:
|
||||
title=share.title,
|
||||
created_at=share.created_at,
|
||||
updated_at=share.updated_at,
|
||||
expires_at=share.expires_at,
|
||||
)
|
||||
|
||||
|
||||
@@ -45,7 +67,12 @@ def create_share(payload: ShareCreateIn, session: DbSession) -> ShareCreatedOut:
|
||||
response_model=SharePublicOut,
|
||||
summary="Lire une carte partagee (public, lecture seule)",
|
||||
)
|
||||
def read_share(share_token: str, session: DbSession) -> SharePublicOut:
|
||||
def read_share(
|
||||
share_token: str,
|
||||
session: DbSession,
|
||||
_sharing: SharingEnabled,
|
||||
_rate: RateLimitRead,
|
||||
) -> SharePublicOut:
|
||||
share = share_service.get_share(session, share_token)
|
||||
return SharePublicOut(
|
||||
share_token=share.share_token,
|
||||
@@ -54,6 +81,7 @@ def read_share(share_token: str, session: DbSession) -> SharePublicOut:
|
||||
map=share_service.load_map_data(share),
|
||||
created_at=share.created_at,
|
||||
updated_at=share.updated_at,
|
||||
expires_at=share.expires_at,
|
||||
)
|
||||
|
||||
|
||||
@@ -67,6 +95,8 @@ def update_share(
|
||||
payload: ShareCreateIn,
|
||||
session: DbSession,
|
||||
management_token: ManagementToken,
|
||||
_sharing: SharingEnabled,
|
||||
_rate: RateLimitWrite,
|
||||
) -> ShareUpdatedOut:
|
||||
share = share_service.update_share(
|
||||
session,
|
||||
@@ -80,6 +110,64 @@ def update_share(
|
||||
share_path=_share_path(share.share_token),
|
||||
title=share.title,
|
||||
updated_at=share.updated_at,
|
||||
expires_at=share.expires_at,
|
||||
)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{share_token}",
|
||||
response_model=ShareOwnerOut,
|
||||
summary="Modifier l'expiration du partage (necessite le jeton de gestion)",
|
||||
)
|
||||
def update_expiry(
|
||||
share_token: str,
|
||||
payload: ShareExpiryIn,
|
||||
session: DbSession,
|
||||
management_token: ManagementToken,
|
||||
_sharing: SharingEnabled,
|
||||
_rate: RateLimitWrite,
|
||||
) -> ShareOwnerOut:
|
||||
share = share_service.set_expiry(
|
||||
session,
|
||||
share_token,
|
||||
management_token=management_token,
|
||||
expires_in_days=None if payload.permanent else payload.expires_in_days,
|
||||
)
|
||||
return ShareOwnerOut(
|
||||
share_token=share.share_token,
|
||||
title=share.title,
|
||||
created_at=share.created_at,
|
||||
updated_at=share.updated_at,
|
||||
expires_at=share.expires_at,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{share_token}/verify",
|
||||
response_model=ShareOwnerOut,
|
||||
summary="Verifier qu'une cle de gestion correspond a ce partage",
|
||||
)
|
||||
def verify_share(
|
||||
share_token: str,
|
||||
session: DbSession,
|
||||
management_token: ManagementToken,
|
||||
_sharing: SharingEnabled,
|
||||
_rate: RateLimitWrite,
|
||||
) -> ShareOwnerOut:
|
||||
"""Permet de reimporter une cle de gestion depuis un autre navigateur.
|
||||
|
||||
Ne renvoie aucune donnee de carte : seulement de quoi reconstituer la fiche
|
||||
de partage locale.
|
||||
"""
|
||||
share = share_service.verify_management_token(
|
||||
session, share_token, management_token=management_token
|
||||
)
|
||||
return ShareOwnerOut(
|
||||
share_token=share.share_token,
|
||||
title=share.title,
|
||||
created_at=share.created_at,
|
||||
updated_at=share.updated_at,
|
||||
expires_at=share.expires_at,
|
||||
)
|
||||
|
||||
|
||||
@@ -93,6 +181,8 @@ def delete_share(
|
||||
share_token: str,
|
||||
session: DbSession,
|
||||
management_token: ManagementToken,
|
||||
_sharing: SharingEnabled,
|
||||
_rate: RateLimitWrite,
|
||||
) -> Response:
|
||||
share_service.delete_share(session, share_token, management_token=management_token)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
Reference in New Issue
Block a user