Instruction file imported from LittleDan9/markdown-manager (
.github/instructions/copilot-sharing-preview.instructions.md). Copyright stays with the author.
Document Sharing & Shared View Instructions
Two Distinct Sharing Systems
This project has two independent sharing mechanisms — don't confuse them:
| System | Purpose | Auth Required | Backend |
|---|---|---|---|
| Public Share Links (this file) | Anonymous read-only access via URL | No | sharing.py, public.py |
Collaboration (see copilot-collaboration.instructions.md) |
Real-time co-editing by invited users | Yes (JWT) | collaboration.py, ws.py |
The ShareModal contains UI for both — a share-link tab and a collaborators tab. The public share link flow is covered here.
Public Share Link Flow
Backend
Token lifecycle:
- Owner calls
POST /api/documents/{id}/share→ generatesshare_token(64-char random hex), setsis_shared=True - Token + flag stored on the
documentstable (share_token,is_sharedcolumns) - Anonymous user hits
GET /api/shared/{share_token}→ returnsSharedDocument(limited fields, no auth) - Owner calls
DELETE /api/documents/{id}/share→ clears token, setsis_shared=False
Key files:
routers/documents/sharing.py—_require_owner()guard ensures only the document owner can enable/disable sharing. Returns 403 for collaborators.routers/public.py— Anonymous endpoint. Loads document viaget_by_share_token(), reads content from filesystem usingcreate_document_response(), includesauthor_namefrom owner relationship.schemas/document.py—ShareResponse(token + is_shared) andSharedDocument(id, name, content, category, folder_path, updated_at, author_name).
Permission rules:
- Only the document owner (
document.user_id == current_user.id) can enable/disable sharing - Collaborators cannot manage share links —
_require_owner()returns 403 - The
ShareButtoncomponent hides entirely for collab documents (currentDocument.user_id !== user.id)
Frontend
Share URL format: {window.location.origin}/shared/{share_token}
The URL is always constructed client-side from the token — the backend never sends a full URL.
Component flow:
ShareButton → opens ShareModal
→ ShareModal "Share Link" tab:
Enable sharing → POST /api/documents/{id}/share → display copy-able URL
Disable sharing → DELETE /api/documents/{id}/share → clear URL
Anonymous Shared View
When a user visits /shared/{token}, the app enters a special read-only mode.
Route Detection (useSharedViewState)
URL: /shared/{token}
→ regex match /^\/shared\/([^/]+)$/
→ DocumentStorageService.clearAllData() (prevents stale auth data leaking)
→ DocumentService.getSharedDocument(token) → sets sharedDocument state
→ isSharedView = true
State shape:
{
isSharedView: boolean, // true when on /shared/* route
sharedDocument: object, // { id, name, content, category, updated_at, author_name }
sharedLoading: boolean,
sharedError: string|null,
shareToken: string|null,
exitSharedView: () => void // navigates to / and clears state
}
Layout Switch (App.jsx)
When isSharedView === true, App.jsx renders SharedViewLayout instead of AppLayout:
- No editor pane — renderer only
- No file browser, no category tabs
- Toolbar shows document name, author, category, last updated (read-only)
- "Exit" button calls
exitSharedView()→ navigates to/
Content Loading (useSharedViewEffects)
Simple effect: when isSharedView && sharedDocument, pushes sharedDocument.content into the global setContent() so the renderer displays it.
Collab-Aware Restrictions
The ShareButton hides for non-owner users:
const isCollabDocument = currentDocument && user && currentDocument.user_id !== user.id;
if (!isAuthenticated || isCollabDocument) return null;
Similarly, Document.jsx (toolbar) disables category dropdown and title rename for collab documents.
File Inventory
Backend
| File | Purpose |
|---|---|
routers/documents/sharing.py |
Owner-only enable/disable share endpoints |
routers/public.py |
Anonymous GET /shared/{token} endpoint |
crud/document.py |
enable_sharing(), disable_sharing(), get_by_share_token() |
schemas/document.py |
ShareResponse, SharedDocument schemas |
Frontend
| File | Purpose |
|---|---|
components/shared/ShareButton.jsx |
Toolbar share button (owner-only) |
components/shared/modals/ShareModal.jsx |
Share link + collaborator management modal |
hooks/ui/useSharedViewState.js |
Route detection + shared doc fetch |
hooks/ui/useSharedViewEffects.js |
Loads shared content into renderer |
components/layout/SharedViewLayout.jsx |
Read-only shared view layout |
components/App.jsx |
Layout switching based on isSharedView |
api/documentsApi.js |
enableSharing(), disableSharing(), getSharedDocument() |
services/core/DocumentService.js |
Service-layer sharing wrappers |
Patterns
Adding Fields to Shared View
- Add field to
SharedDocumentschema inschemas/document.py - Populate it in
routers/public.pyget_shared_document()from the document/owner model - Access it in frontend via
sharedDocument.fieldNamein the shared view components
Testing Share Links
# Get auth token
TOKEN=$(curl -s -X POST http://localhost/api/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"user@example.com","password":"pass"}' | jq -r .access_token)
# Enable sharing
SHARE=$(curl -s -X POST http://localhost/api/documents/1/share \
-H "Authorization: Bearer $TOKEN" | jq -r .share_token)
# Access anonymously
curl -s http://localhost/api/shared/$SHARE | jq .name
# Disable sharing
curl -s -X DELETE http://localhost/api/documents/1/share \
-H "Authorization: Bearer $TOKEN"