feat(share): Nur-Ansicht-Teilen-Link (live, optional PIN) + QR (v1.6.9)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 31m39s

Neuer öffentlicher Teilen-Link für die aktuelle Lage — ohne Login nur zum
Ansehen, live aktualisiert. Optional mit 4–8-stelliger PIN geschützt.

- DB: Project.shareToken (unique) / sharePin (bcrypt) / shareEnabled + Migration
- API: POST/GET /api/projects/[id]/share (Verwaltung, Mandanten-Check, VIEWER blockiert),
  GET/POST /api/share/[token] (öffentlich, PIN-Prüfung, Rate-Limit gegen Brute-Force)
- Öffentliche Seite /view/[token]: PIN-Gate, read-only MapView, Socket-Live + 30s-Poll
- Teilen-Dialog im Werkzeuge-/Mobil-Menü: Link kopieren, QR-Code, PIN setzen/entfernen,
  Teilen beenden (widerruft den Link)
- Middleware: /api/share/ öffentlich freigeschaltet

Hinweis: hochgeladene Custom-Symbole zeigen im anonymen Viewer den Platzhalter
(Icon-Endpunkte bleiben auth-geschützt); eingebaute taktische Zeichen erscheinen normal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-22 23:30:47 +02:00
parent eb5da2f0c3
commit 6aa70e1504
10 changed files with 630 additions and 1 deletions

View File

@@ -47,8 +47,10 @@ import {
Plus,
ChevronDown,
Loader2,
Share2,
} from 'lucide-react'
import { HoseSettingsDialog } from '@/components/dialogs/hose-settings-dialog'
import { ShareDialog } from '@/components/dialogs/share-dialog'
import { degToCompass } from '@/components/map/map-compass'
import type { Project, DrawFeature, ProjectMode } from '@/types'
import { formatDateTime } from '@/lib/utils'
@@ -119,6 +121,7 @@ export function Topbar({
}: TopbarProps) {
const [isLoadDialogOpen, setIsLoadDialogOpen] = useState(false)
const [isHoseSettingsOpen, setIsHoseSettingsOpen] = useState(false)
const [isShareOpen, setIsShareOpen] = useState(false)
const [showPasswordDialog, setShowPasswordDialog] = useState(false)
const [showDeleteAccountDialog, setShowDeleteAccountDialog] = useState(false)
const [deleteAccountPw, setDeleteAccountPw] = useState('')
@@ -293,6 +296,12 @@ export function Topbar({
<List className="w-4 h-4 mr-2" />
Einsätze verwalten
</DropdownMenuItem>
{project && (
<DropdownMenuItem onClick={() => setIsShareOpen(true)}>
<Share2 className="w-4 h-4 mr-2" />
Lage teilen (Nur-Ansicht)
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => handleExport('png')}>
<Download className="w-4 h-4 mr-2" />
@@ -417,6 +426,12 @@ export function Topbar({
<List className="w-4 h-4 mr-2" />
Einsätze verwalten
</DropdownMenuItem>
{project && (
<DropdownMenuItem onClick={() => setIsShareOpen(true)}>
<Share2 className="w-4 h-4 mr-2" />
Lage teilen (Nur-Ansicht)
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
@@ -641,6 +656,8 @@ export function Topbar({
<HoseSettingsDialog open={isHoseSettingsOpen} onOpenChange={setIsHoseSettingsOpen} />
<ShareDialog open={isShareOpen} onOpenChange={setIsShareOpen} projectId={project?.id ?? null} projectTitle={project?.title} />
{/* Password Change Dialog */}
<Dialog open={showPasswordDialog} onOpenChange={(open) => {
setShowPasswordDialog(open)