Files
GearBox/.planning/phases/32-setup-sharing-system/32-04-PLAN.md
Jean-Luc Makiola 338a78122d docs(32): fix wave assignment — Plan 04 bumped to wave 4
Plans 03 and 04 both modify setups/$setupId.tsx. Per wave assignment
rules, file overlap requires sequential execution. Plan 04 now depends
on Plan 03 and runs in Wave 4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:06:39 +02:00

8.6 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
32-setup-sharing-system 04 execute 4
01
02
03
src/client/routes/setups/$setupId.tsx
src/client/hooks/useSetups.ts
true
TBD
truths artifacts key_links
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
path provides
src/client/routes/setups/$setupId.tsx Enhanced setup detail page with share token detection and shared view mode
path provides exports
src/client/hooks/useSetups.ts useSharedSetup hook for fetching shared setup data
useSharedSetup
from to via pattern
src/client/routes/setups/$setupId.tsx src/client/hooks/useSetups.ts useSharedSetup hook for share token access useSharedSetup
from to via pattern
src/client/routes/setups/$setupId.tsx /api/shared/:token API fetch for shared setup data 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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):

// 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:

// 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`:**
export function useSharedSetup(token: string | null) {
  return useQuery({
    queryKey: ["shared-setup", token],
    queryFn: () => apiGet<SetupWithItems>(`/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:
import { z } from "zod";

export const Route = createFileRoute("/setups/$setupId")({
  component: SetupDetailPage,
  validateSearch: z.object({
    share: z.string().optional(),
  }),
});
  1. In SetupDetailPage, detect the share token:
const { share: shareToken } = Route.useSearch();
  1. Update the three-way data source logic:
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;
  1. Add shared banner (per 32-UI-SPEC.md) — render above the header bar when isSharedView:
{isSharedView && setup && (
  <div className="flex items-center gap-2 px-4 py-2 bg-blue-50 border-b border-blue-100">
    <LucideIcon name="link" size={16} className="text-blue-500" />
    <span className="text-sm text-blue-700">Shared setup</span>
  </div>
)}
  1. Add error state for invalid/expired share tokens:
{isSharedView && isError && (
  <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 text-center">
    <LucideIcon name="link" size={48} className="text-gray-300 mx-auto mb-4" />
    <h2 className="text-xl font-semibold text-gray-900 mb-2">Link not available</h2>
    <p className="text-sm text-gray-500">This share link has expired or is no longer valid.</p>
  </div>
)}
  1. 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 && ( ... )}

  2. 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" <acceptance_criteria>

    • 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 </acceptance_criteria> Shared setup viewer with token detection, shared banner, error handling, and read-only mode

<threat_model>

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
</threat_model>
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

<success_criteria>

  • 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) </success_criteria>
After completion, create `.planning/phases/32-setup-sharing-system/32-04-SUMMARY.md`