feat(sharing): add document sharing with expiring links

- Add SharedLink model with token, expiry, view limit, password hash
- Add migration 025_add_shared_links
- Add API endpoints: create, list, revoke (auth) + public info/download
- Add management UI at /shared-links with revoke controls
- Add public share landing page at /share/{token}
- Add Share button on file_view.html
- Add Shared Links to user dropdown in common.js
- Write 35 unit tests covering all scenarios
- Update UserGuide.md with sharing documentation

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-08 21:51:30 +00:00
parent 93b3959d2f
commit 0f91b8bb7c
13 changed files with 2170 additions and 0 deletions
+4
View File
@@ -25,6 +25,8 @@ from app.views.plans import router as plans_router # Admin Plan Designer
from app.views.queue import router as queue_router
from app.views.search import router as search_router
from app.views.settings import router as settings_router
from app.views.share import router as share_router
from app.views.shared_links import router as shared_links_router
from app.views.status import router as status_router
from app.views.subscriptions import router as subscriptions_router # Pricing + subscription pages
from app.views.wizard import router as wizard_router
@@ -35,6 +37,8 @@ router.include_router(wizard_router) # Wizard first (for /setup)
router.include_router(db_wizard_router) # Database wizard
router.include_router(admin_users_router) # Admin user management
router.include_router(api_tokens_router) # API token management
router.include_router(shared_links_router) # Shared links management
router.include_router(share_router) # Public share landing pages (no auth)
router.include_router(backup_router) # Backup dashboard
router.include_router(general_router)
router.include_router(status_router)
+28
View File
@@ -0,0 +1,28 @@
"""Public view for accessing a shared document link.
This route does NOT require authentication — it is the landing page
that link recipients visit. The page fetches link metadata via the
public ``/api/share/{token}/info`` JSON endpoint and then renders the
appropriate download UI (password gate or direct download button).
"""
import logging
import pathlib
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates
logger = logging.getLogger(__name__)
router = APIRouter()
_templates_dir = pathlib.Path(__file__).parents[2] / "frontend" / "templates"
templates = Jinja2Templates(directory=str(_templates_dir))
@router.get("/share/{token}")
async def shared_link_view(request: Request, token: str):
"""Render the public share landing page for a given token."""
return templates.TemplateResponse(
"shared_link_view.html",
{"request": request, "token": token},
)
+24
View File
@@ -0,0 +1,24 @@
"""View route for the Shared Links management page.
Renders the ``shared_links.html`` template where authenticated users can
create, view, and revoke their document share links.
"""
import logging
from fastapi import APIRouter, Request
from app.views.base import require_login, templates
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/shared-links")
@require_login
async def shared_links_page(request: Request):
"""Render the Shared Links management page."""
return templates.TemplateResponse(
"shared_links.html",
{"request": request, "page_title": "Shared Links"},
)