docs(32): create phase plans for setup sharing system
4 plans in 3 waves: - Wave 1: Schema migration (isPublic→visibility) + shares table - Wave 2: Share link service + API routes - Wave 3: Share modal UI + shared setup viewer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
231
.planning/phases/32-setup-sharing-system/32-04-PLAN.md
Normal file
231
.planning/phases/32-setup-sharing-system/32-04-PLAN.md
Normal file
@@ -0,0 +1,231 @@
|
||||
---
|
||||
phase: 32-setup-sharing-system
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: [01, 02]
|
||||
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>
|
||||
Reference in New Issue
Block a user