OpenMind v1 : cartes mentales libres, local-first et dockerisees
Premiere version fonctionnelle, demarrable avec `docker compose up -d --build` sur http://localhost:8080. Architecture - Frontend React 18 / TypeScript strict / Vite / Tailwind, canvas @xyflow/react, etat Zustand, persistance IndexedDB via Dexie, auto-layout dagre. - Backend FastAPI / SQLAlchemy 2 / Pydantic v2 / SQLite, qui sert aussi les fichiers statiques du frontend compile. - Image Docker multi-stage, un seul service, bind mount ./data pour SQLite. Local-first Les cartes restent dans le navigateur. Le serveur ne recoit une carte que sur demande explicite de partage. Partage Deux secrets distincts : share_token public en lecture seule dans l'URL, et management_token conserve uniquement cote client, transmis en en-tete pour les modifications et stocke hashe en base. Fonctionnalites Tableau de bord, editeur complet (undo/redo, copier/coller, raccourcis, branches repliables, recherche), 9 themes, 4 dispositions automatiques, panneau de proprietes, import/export .mmap, export PNG/SVG/PDF, impression, page publique en lecture seule, PWA. Qualite 25 tests pytest et 43 tests vitest au vert, ruff et ESLint sans erreur, TypeScript strict sans erreur. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
98
backend/app/api/shares.py
Normal file
98
backend/app/api/shares.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""API REST de partage de cartes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Response, status
|
||||
|
||||
from app.api.deps import DbSession, ManagementToken
|
||||
from app.schemas.share import (
|
||||
ShareCreatedOut,
|
||||
ShareCreateIn,
|
||||
SharePublicOut,
|
||||
ShareUpdatedOut,
|
||||
)
|
||||
from app.services import shares as share_service
|
||||
|
||||
router = APIRouter(prefix="/shares", tags=["shares"])
|
||||
|
||||
|
||||
def _share_path(share_token: str) -> str:
|
||||
return f"/s/{share_token}"
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=ShareCreatedOut,
|
||||
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:
|
||||
share, management_token = share_service.create_share(
|
||||
session, title=payload.title, document=payload.map
|
||||
)
|
||||
return ShareCreatedOut(
|
||||
share_token=share.share_token,
|
||||
management_token=management_token,
|
||||
share_path=_share_path(share.share_token),
|
||||
title=share.title,
|
||||
created_at=share.created_at,
|
||||
updated_at=share.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{share_token}",
|
||||
response_model=SharePublicOut,
|
||||
summary="Lire une carte partagee (public, lecture seule)",
|
||||
)
|
||||
def read_share(share_token: str, session: DbSession) -> SharePublicOut:
|
||||
share = share_service.get_share(session, share_token)
|
||||
return SharePublicOut(
|
||||
share_token=share.share_token,
|
||||
title=share.title,
|
||||
map_version=share.map_version,
|
||||
map=share_service.load_map_data(share),
|
||||
created_at=share.created_at,
|
||||
updated_at=share.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{share_token}",
|
||||
response_model=ShareUpdatedOut,
|
||||
summary="Mettre a jour le snapshot partage (necessite le jeton de gestion)",
|
||||
)
|
||||
def update_share(
|
||||
share_token: str,
|
||||
payload: ShareCreateIn,
|
||||
session: DbSession,
|
||||
management_token: ManagementToken,
|
||||
) -> ShareUpdatedOut:
|
||||
share = share_service.update_share(
|
||||
session,
|
||||
share_token,
|
||||
management_token=management_token,
|
||||
title=payload.title,
|
||||
document=payload.map,
|
||||
)
|
||||
return ShareUpdatedOut(
|
||||
share_token=share.share_token,
|
||||
share_path=_share_path(share.share_token),
|
||||
title=share.title,
|
||||
updated_at=share.updated_at,
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{share_token}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
response_class=Response,
|
||||
summary="Supprimer le partage (necessite le jeton de gestion)",
|
||||
)
|
||||
def delete_share(
|
||||
share_token: str,
|
||||
session: DbSession,
|
||||
management_token: ManagementToken,
|
||||
) -> 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