--- phase: 32-setup-sharing-system plan: 04 type: execute wave: 4 depends_on: [01, 02, 03] files_modified: - src/client/routes/setups/$setupId.tsx - src/client/hooks/useSetups.ts autonomous: true requirements: - TBD must_haves: truths: - "Anonymous user visiting /setups/:id?share=token sees the shared setup with items" - "Shared setup viewer shows a 'Shared setup' banner at the top" - "Invalid or expired share tokens show an error message" - "Short URL /s/:token redirects to /setups/:id?share=token" - "Shared viewer is read-only — no edit buttons, no share button, no delete button" artifacts: - path: "src/client/routes/setups/$setupId.tsx" provides: "Enhanced setup detail page with share token detection and shared view mode" - path: "src/client/hooks/useSetups.ts" provides: "useSharedSetup hook for fetching shared setup data" exports: ["useSharedSetup"] key_links: - from: "src/client/routes/setups/$setupId.tsx" to: "src/client/hooks/useSetups.ts" via: "useSharedSetup hook for share token access" pattern: "useSharedSetup" - from: "src/client/routes/setups/$setupId.tsx" to: "/api/shared/:token" via: "API fetch for shared setup data" pattern: "api/shared" --- Add shared setup viewer functionality to the existing setup detail page — detect share token in URL, fetch via shared endpoint, and display read-only view with shared banner. Purpose: This completes the user-facing share flow (D-06, D-17). When someone receives a share link, they can view the setup without authentication. Output: Updated setup detail page with share token detection and shared viewing mode. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/32-setup-sharing-system/32-CONTEXT.md @.planning/phases/32-setup-sharing-system/32-UI-SPEC.md @.planning/phases/32-setup-sharing-system/32-01-SUMMARY.md @.planning/phases/32-setup-sharing-system/32-02-SUMMARY.md Shared access API endpoint (from Plan 02): ``` GET /api/shared/:token → Setup object with items array (same format as public view) Returns 404 for invalid/expired/revoked tokens ``` Short URL redirect (from Plan 02): ``` GET /s/:token → 302 redirect to /setups/:setupId?share=:token ``` From src/client/routes/setups/$setupId.tsx (current structure): ```typescript // Three-way data source: private (auth), public (no auth), shared (token) const { data: auth } = useAuth(); const isAuthenticated = !!auth?.user; const privateSetup = useSetup(isAuthenticated ? numericId : null); const publicSetup = usePublicSetup(!isAuthenticated ? numericId : null); ``` From @tanstack/react-router: ```typescript // URL search params access const search = Route.useSearch(); // needs searchSchema defined on route ``` Task 1: Add useSharedSetup hook and share token detection to setup detail page src/client/hooks/useSetups.ts, src/client/routes/setups/$setupId.tsx src/client/hooks/useSetups.ts, src/client/routes/setups/$setupId.tsx, src/client/lib/api.ts **Add `useSharedSetup` hook to `src/client/hooks/useSetups.ts`:** ```typescript export function useSharedSetup(token: string | null) { return useQuery({ queryKey: ["shared-setup", token], queryFn: () => apiGet(`/api/shared/${token}`), enabled: !!token, retry: false, // Don't retry on 404 }); } ``` Use the same `SetupWithItems` type used by `useSetup` and `usePublicSetup`. **Update `src/client/routes/setups/$setupId.tsx`:** 1. Add search params validation to the route definition to capture the `share` query param: ```typescript import { z } from "zod"; export const Route = createFileRoute("/setups/$setupId")({ component: SetupDetailPage, validateSearch: z.object({ share: z.string().optional(), }), }); ``` 2. In `SetupDetailPage`, detect the share token: ```typescript const { share: shareToken } = Route.useSearch(); ``` 3. Update the three-way data source logic: ```typescript const { data: auth } = useAuth(); const isAuthenticated = !!auth?.user; // Priority: share token > authenticated owner > public viewer const sharedSetup = useSharedSetup(shareToken ?? null); const privateSetup = useSetup(!shareToken && isAuthenticated ? numericId : null); const publicSetup = usePublicSetup(!shareToken && !isAuthenticated ? numericId : null); const isSharedView = !!shareToken; const { data: setup, isLoading, isError } = isSharedView ? sharedSetup : isAuthenticated ? privateSetup : publicSetup; ``` 4. Add shared banner (per 32-UI-SPEC.md) — render above the header bar when `isSharedView`: ```tsx {isSharedView && setup && (
Shared setup
)} ``` 5. Add error state for invalid/expired share tokens: ```tsx {isSharedView && isError && (

Link not available

This share link has expired or is no longer valid.

)} ``` 6. Hide owner-only controls when in shared view — conditionally hide these elements when `isSharedView` is true: - Add Items button (both desktop and mobile variants) - Share button (both desktop and mobile variants) - Delete Setup button (both desktop and mobile variants) - Classification dropdowns on items - Remove item buttons Wrap each with: `{!isSharedView && isAuthenticated && ( ... )}` 7. The shared view shows the same read-only content as the public view: item list grouped by category, weight summary card, setup name header.
grep -q "useSharedSetup" src/client/hooks/useSetups.ts && grep -q "shareToken\|share:" src/client/routes/setups/\$setupId.tsx && grep -q "Shared setup" src/client/routes/setups/\$setupId.tsx && bun run lint && echo "PASS" || echo "FAIL" - `src/client/hooks/useSetups.ts` exports `useSharedSetup(token)` that fetches `/api/shared/:token` - `src/client/routes/setups/$setupId.tsx` validates `share` search param via Zod - When `?share=token` is present, setup data is fetched via shared endpoint (not owner or public) - Shared banner (`Shared setup` with link icon in blue-50) appears at top of page when share token present - Invalid/expired token shows error state with "Link not available" message - Owner-only controls (add items, share, delete, classification, remove item) are hidden in shared view - `bun run lint` passes Shared setup viewer with token detection, shared banner, error handling, and read-only mode
## Trust Boundaries | Boundary | Description | |----------|-------------| | URL search params | Share token from URL — untrusted user input | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-32-09 | Spoofing | share token in URL | mitigate | Token validated server-side by /api/shared/:token — client only passes through, no client-side authorization decisions | | T-32-10 | Information Disclosure | shared view content | accept | Shared setup data is intentionally visible to anyone with the token — this is the feature | 1. `bun run lint` passes 2. Visit `/setups/1?share=valid-token` — shows setup with shared banner, no edit controls 3. Visit `/setups/1?share=invalid-token` — shows error state 4. Visit `/s/valid-token` — redirects to `/setups/:id?share=token`, displays shared view 5. Owner visiting their own setup normally (no share param) — sees all controls as before - Share links use `/s/{token}` short URL AND `/setups/:id?share={token}` (per D-06) - Shared setup viewer works for anonymous users (per D-17) - No owner-only actions visible in shared view - No changes to discovery feed or profile page (per D-18) After completion, create `.planning/phases/32-setup-sharing-system/32-04-SUMMARY.md`