diff --git a/.beans/nuzlocke-tracker-i2va--hide-edit-controls-for-non-owners-in-frontend.md b/.beans/nuzlocke-tracker-i2va--hide-edit-controls-for-non-owners-in-frontend.md new file mode 100644 index 0000000..491344c --- /dev/null +++ b/.beans/nuzlocke-tracker-i2va--hide-edit-controls-for-non-owners-in-frontend.md @@ -0,0 +1,41 @@ +--- +# nuzlocke-tracker-i2va +title: Hide edit controls for non-owners in frontend +status: in-progress +type: bug +priority: critical +created_at: 2026-03-21T12:18:38Z +updated_at: 2026-03-21T12:32:45Z +parent: nuzlocke-tracker-wwnu +blocked_by: + - nuzlocke-tracker-73ba +--- + +## Problem + +`RunEncounters.tsx` has NO auth checks — all edit buttons (encounter modals, boss defeat, status changes, end run, shiny encounters, egg encounters, transfers, HoF team) are always visible, even to logged-out users viewing a public run. + +`RunDashboard.tsx` has `canEdit = isOwner || !run?.owner` (line 70) which means unowned legacy runs are editable by anyone, including logged-out users. + +## Approach + +1. Add `useAuth` and `canEdit` logic to `RunEncounters.tsx`, matching the pattern from `RunDashboard.tsx` but stricter: `canEdit = isOwner` (no fallback for unowned runs) +2. Update `RunDashboard.tsx` line 70 to `canEdit = isOwner` (remove `|| !run?.owner`) +3. Conditionally render all mutation UI elements based on `canEdit`: + - Encounter create/edit modals and triggers + - Boss defeat buttons + - Status change / End run buttons + - Shiny encounter / Egg encounter modals + - Transfer modal + - HoF team modal + - Visibility settings toggle +4. Show a read-only banner when viewing someone else's run + +## Checklist + +- [x] Add `useAuth` import and `canEdit` logic to `RunEncounters.tsx` +- [x] Guard all mutation triggers in `RunEncounters.tsx` behind `canEdit` +- [x] Update `RunDashboard.tsx` `canEdit` to be `isOwner` only (no unowned fallback) +- [x] Guard all mutation triggers in `RunDashboard.tsx` behind `canEdit` +- [x] Add read-only indicator/banner for non-owner viewers +- [x] Verify logged-out users see no edit controls on public runs diff --git a/frontend/src/pages/RunDashboard.tsx b/frontend/src/pages/RunDashboard.tsx index b8bd8c5..784e90e 100644 --- a/frontend/src/pages/RunDashboard.tsx +++ b/frontend/src/pages/RunDashboard.tsx @@ -67,7 +67,7 @@ export function RunDashboard() { const [teamSort, setTeamSort] = useState('route') const isOwner = user && run?.owner?.id === user.id - const canEdit = isOwner || !run?.owner + const canEdit = isOwner const encounters = run?.encounters ?? [] const alive = useMemo( @@ -143,6 +143,32 @@ export function RunDashboard() { + {/* Read-only Banner */} + {!canEdit && run.owner && ( +
+
+ + + + + + Viewing {run.owner.displayName ? `${run.owner.displayName}'s` : "another player's"}{' '} + run (read-only) + +
+
+ )} + {/* Completion Banner */} {!isActive && (
isExpanded: boolean onToggleExpand: () => void - onRouteClick: (route: Route) => void + onRouteClick: ((route: Route) => void) | undefined filter: 'all' | RouteStatus pinwheelClause: boolean } @@ -438,10 +439,12 @@ function RouteGroup({
- {isActive && run.rules?.shinyClause && ( + {isActive && canEdit && run.rules?.shinyClause && ( )} - {isActive && ( + {isActive && canEdit && ( )} - {isActive && ( + {isActive && canEdit && (
+ {/* Read-only Banner */} + {!canEdit && run.owner && ( +
+
+ + + + + + Viewing {run.owner.displayName ? `${run.owner.displayName}'s` : "another player's"}{' '} + run (read-only) + +
+
+ )} + {/* Completion Banner */} {!isActive && (
- {run.status === 'completed' && run.genlocke && !run.genlocke.isFinalLeg && ( + {run.status === 'completed' && run.genlocke && !run.genlocke.isFinalLeg && canEdit && ( + {canEdit && ( + + )} {hofTeam ? (
@@ -1262,7 +1297,9 @@ export function RunEncounters() { setSelectedTeamEncounter(enc) : undefined} + onClick={ + isActive && canEdit ? () => setSelectedTeamEncounter(enc) : undefined + } /> ))}
@@ -1276,7 +1313,9 @@ export function RunEncounters() { key={enc.id} encounter={enc} showFaintLevel - onClick={isActive ? () => setSelectedTeamEncounter(enc) : undefined} + onClick={ + isActive && canEdit ? () => setSelectedTeamEncounter(enc) : undefined + } /> ))} @@ -1292,7 +1331,9 @@ export function RunEncounters() {
setSelectedTeamEncounter(enc) : undefined} + onEncounterClick={ + isActive && canEdit ? (enc) => setSelectedTeamEncounter(enc) : undefined + } />
)} @@ -1306,7 +1347,7 @@ export function RunEncounters() { setSelectedTeamEncounter(enc) : undefined} + onClick={isActive && canEdit ? () => setSelectedTeamEncounter(enc) : undefined} /> ))} @@ -1318,7 +1359,7 @@ export function RunEncounters() {

Encounters

- {isActive && completedCount < totalLocations && ( + {isActive && canEdit && completedCount < totalLocations && (