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>
232 lines
8.6 KiB
Markdown
232 lines
8.6 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
|
|
<interfaces>
|
|
<!-- Key types and contracts from Plans 01 and 02. -->
|
|
|
|
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
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add useSharedSetup hook and share token detection to setup detail page</name>
|
|
<files>src/client/hooks/useSetups.ts, src/client/routes/setups/$setupId.tsx</files>
|
|
<read_first>src/client/hooks/useSetups.ts, src/client/routes/setups/$setupId.tsx, src/client/lib/api.ts</read_first>
|
|
<action>
|
|
**Add `useSharedSetup` hook to `src/client/hooks/useSetups.ts`:**
|
|
|
|
```typescript
|
|
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:
|
|
```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 && (
|
|
<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>
|
|
)}
|
|
```
|
|
|
|
5. Add error state for invalid/expired share tokens:
|
|
```tsx
|
|
{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>
|
|
)}
|
|
```
|
|
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>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"</automated>
|
|
</verify>
|
|
<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>
|
|
<done>Shared setup viewer with token detection, shared banner, error handling, and read-only mode</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<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>
|
|
|
|
<verification>
|
|
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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/32-setup-sharing-system/32-04-SUMMARY.md`
|
|
</output>
|