Skip to content

Access Controls#11

Merged
jwhumphries merged 24 commits into
mainfrom
dev
Jul 5, 2026
Merged

Access Controls#11
jwhumphries merged 24 commits into
mainfrom
dev

Conversation

@jwhumphries

@jwhumphries jwhumphries commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added an admin area (/admin) for managing users, bands, and signup access policy, gated to authorized admin accounts.
    • Added an “Admin” navigation item for admins and an isAdmin flag to the current-user response.
    • Added a fixed footer with a GitHub profile link and the current year.
  • Bug Fixes

    • Improved signup access-policy enforcement (registration closed behavior) and enhanced secret URL redaction.
    • Improved join-page error handling for transient failures and preview issues.
  • Chores

    • Updated local dev Git ignore rules to exclude .worktrees/.

jwhumphries and others added 22 commits July 3, 2026 18:34
Cloudflare Access onboarding was too much friction; this replaces it with
an in-app admin panel (user/band list+delete) and a DB-backed signup
allow-list, gated by an env-based admin-email set that never touches git.
11 TDD tasks: admin-email config, AccessPolicy/AllowedEmail models,
repo CRUD, DeleteUser cascade (factored from DeleteBand/DeleteSong),
signup enforcement, RequireAdmin middleware, admin endpoints, and the
frontend admin panel (users/bands/access-policy tabs).
- Add AdminEmails map[string]bool field to API struct
- Implement IsAdminEmail method for case-insensitive email lookup
- Add BANDWIDTH_ADMIN_EMAILS env var with empty default to viper config
- Add parseAdminEmails helper to parse comma-separated admin email list
- Wire admin_emails into API initialization from viper config
- Add comprehensive tests for IsAdminEmail with edge cases

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Register the new models in AutoMigrate and add schema migration test coverage.
Task 3's repository functions will operate on these models.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implement repository layer for access policy management (enable/disable signup gating)
and allowed email list (CRUD operations). Add admin listing queries for all users and
all bands with necessary metadata for admin panel display.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracts deleteBandTx and deleteSongRowsTx (pure refactor, verified against
the existing test suite) so the new Repo.DeleteUser can cascade a created
band and personal songs without nesting transactions. DeleteUser also
clears the deleted user's sessions, backup codes, password resets,
personal folders, memberships, personal-layer rows on other bands' songs,
and pending invites addressed to them.
Implements the access policy check in the Signup handler. Blocks
registration unless the policy is disabled, the caller is an admin email,
or the email is on the allow-list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Convert userResponse from a package-level function to a method on *API
so it can call a.IsAdminEmail() and add isAdmin field to every endpoint
that returns a user in its JSON payload. Updates 6 call sites across
account.go, auth.go, and twofa.go.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements the RequireAdmin middleware that rejects callers whose email
is not configured as a site admin. Must run after RequireAuth in the
middleware chain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires the Task 3-4 repository methods and Task 7 RequireAdmin middleware
into 8 handler methods under /api/admin/*: list/delete users, list/delete
bands, get/set the signup access policy, and add/remove allow-list
emails. Self-delete is blocked (400) and missing users/bands/entries
404 before delete/remove.
Replaces the shared adminTargetID helper with adminUserID, adminBandID,
and adminAllowedEmailID so malformed :id path params return the same
resource-specific "not found" message as the well-formed-but-missing
case, matching the songID/memberID convention elsewhere.
Implements 8 hooks for admin panel: useAdminUsers, useDeleteAdminUser,
useAdminBands, useDeleteAdminBand, useAccessPolicy, useSetAccessPolicy,
useAddAllowedEmail, useRemoveAllowedEmail. Follows existing hook pattern
with proper query-key invalidation on mutations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolves the App.tsx import that has referenced a nonexistent
./pages/AdminPage module since Task 9. Full just check suite (lint,
typecheck, format, Go tests, frontend tests) now passes clean end to end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
deleteBandTx deleted a band's songs, members, and invites but left its
Folder/FolderEntry rows behind, permanently orphaned since folder
listing is always owner-scoped. DeleteUser now reaches this path when
cascading through bands the deleted user created, so fix it here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets the admin panel be tested locally via `just dev` by exporting the
var on the host shell before starting the dev container.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Invalidate the bands query when an admin deletes a user, since
  DeleteUser cascades to bands that user created
- Add aria-selected to the admin panel's tab buttons
- Add audit logging (actor + target) to all five destructive/mutating
  admin handlers
- Extract deleteOwnedFoldersTx, a shared batched-delete helper, to
  de-duplicate the band/user folder-cleanup logic in deleteBandTx and
  DeleteUser

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add admin panel and signup access policy
A non-descript, fixed-position link in the corner of every page (App.tsx,
above the route tree, so it covers login/signup/join too) — © and a link
to the GitHub profile.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add a small copyright footer link
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds admin-email configuration, access-policy persistence, admin routes and UI, shared row-component updates, a footer, and small server-side and password-reset adjustments.

Changes

Admin Access Policy and Panel

Layer / File(s) Summary
Admin email config and detection
cmd/bandwidth/main.go, cmd/bandwidth/server.go, docker-compose.yml, internal/handlers/api.go, internal/handlers/api_test.go, cmd/bandwidth/server_test.go
Adds admin_emails configuration, parses it into API.AdminEmails, and tests admin-email normalization.
Access policy model and migration
internal/model/accesspolicy.go, internal/repository/repository.go, internal/repository/repository_test.go
Introduces persisted access-policy and allowed-email models and migrates them.
Admin repository APIs
internal/repository/admin.go, internal/repository/admin_test.go
Adds repository methods for user/band listing, access-policy state, and allowed-email CRUD with tests.
Transactional delete refactors
internal/repository/bands.go, internal/repository/songs.go, internal/repository/folders.go, internal/repository/users.go, internal/repository/bandinvites.go, internal/repository/maintenance.go, internal/repository/bands_test.go, internal/repository/users_test.go
Extracts delete helpers and adds cascading DeleteUser, with invite-purge predicate sharing and tests.
Signup gate and admin flag
internal/handlers/auth.go, internal/handlers/auth_test.go, internal/handlers/account.go, internal/handlers/twofa.go
Gates signup on the access policy and changes user serialization to include isAdmin.
Admin middleware and HTTP routes
internal/middleware/auth.go, internal/middleware/auth_test.go, internal/handlers/admin.go, internal/handlers/admin_test.go, cmd/bandwidth/server.go
Adds RequireAdmin, admin handlers, admin route registration, and secret-path redaction updates.
Frontend admin UI and routing
frontend/src/lib/types.ts, frontend/src/hooks/admin.ts, frontend/src/components/RequireAdmin.tsx, frontend/src/components/RequireAdmin.test.tsx, frontend/src/pages/AdminPage.tsx, frontend/src/pages/AdminPage.test.tsx, frontend/src/components/Layout.tsx, frontend/src/App.tsx, frontend/src/pages/JoinPage.tsx, frontend/src/pages/JoinPage.test.tsx
Adds admin types, hooks, guard, page, route wiring, conditional nav, and join-page error handling updates.
Admin access policy docs
docs/superpowers/specs/..., docs/superpowers/plans/...
Adds the design spec and implementation plan for the admin panel and access policy.

Shared Row Component Consolidation

Layer / File(s) Summary
Shared row props and replacements
frontend/src/components/songs/SongRow.tsx, frontend/src/components/folders/SortableFolderRow.tsx, frontend/src/components/folders/SortableSongList.tsx, frontend/src/components/bands/BandSongList.tsx, frontend/src/components/bands/BandFolderSidebar.tsx, frontend/src/pages/HomePage.tsx, frontend/src/components/bands/BandSongRow.tsx, frontend/src/components/bands/BandFolderRow.tsx, frontend/src/components/songs/SongRow.test.tsx, frontend/src/components/folders/SortableFolderRow.test.tsx
Adds linkTo, canEdit, and actionLabel support, swaps in shared row components, and removes the band-specific row components.
Footer and Infrastructure Updates
frontend/src/components/Footer.tsx, frontend/src/components/Footer.test.tsx, frontend/src/App.tsx, cmd/bandwidth/server.go, internal/handlers/passwordreset.go, .gitignore
Adds a footer, runs the initial purge immediately in the background loop, hashes failed password-reset lookups, and ignores .worktrees/.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • jwhumphries/BandWidth#1: This PR extends the auth/session and user-response flow that the admin-email and isAdmin changes build on.
  • jwhumphries/BandWidth#2: The SongRow and song-list updates here build directly on the shared song UI introduced there.
  • jwhumphries/BandWidth#8: The server startup, purge loop, and URL-redaction changes overlap with the same backend paths modified in that PR.

Poem

A rabbit hops through admin gates,
With allow-lists, tabs, and tidy states.
Songs and folders share one row,
Footers glow and secrets go.
Hop-hop — the policy now grows 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main theme of the PR, which adds admin and signup access-control features.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/bandwidth/server.go`:
- Around line 267-278: Add table-driven tests for parseAdminEmails to lock down
its normalization behavior. Create tests that exercise strings with commas,
extra spaces, mixed case, and empty entries, and verify the returned map from
parseAdminEmails contains only trimmed lowercase emails with duplicates/empties
removed. Use the parseAdminEmails function name in the test so it’s easy to
locate and keep the cases concise but comprehensive.

In `@docs/superpowers/plans/2026-07-03-admin-access-policy.md`:
- Around line 1038-1071: markdownlint is flagging the fenced go examples inside
the bullet list because they are missing surrounding blank lines. Update the
markdown in this section so each code fence under the account.go, auth.go, and
twofa.go bullets has an empty line before and after it, keeping the same content
but formatting it as a proper fenced block within the list.

In `@frontend/src/components/folders/SortableFolderRow.tsx`:
- Around line 10-24: `SortableFolderRow` currently gates drag-and-drop and edit
affordances with `canEdit`, but add coverage to prove the disabled state is
fully enforced. Update the tests for `SortableFolderRow` to render with
`canEdit={false}` and assert the grip/rename/delete controls are not present and
the `useSortable` interaction is disabled for that row. Use the component’s
`SortableFolderRow`/`useSortable` behavior in the test setup so the missing
affordances and disabled drag state are verified together.

In `@frontend/src/components/Footer.tsx`:
- Around line 5-10: The Footer link uses global fixed positioning with
bottom-right anchoring and a z-index, which can overlap other UI in pages that
also place controls there. Update Footer so its placement is conditional or
offset-aware, and verify it does not collide with bottom-right elements rendered
by routes like SongPage and BandPage. Use the Footer component and its fixed
positioning styles as the main place to adjust behavior.

In `@frontend/src/components/songs/SongRow.tsx`:
- Around line 9-22: Add test coverage for the new hidden-action branch in
SongRow by asserting the action button is not rendered when SongRow is passed
canEdit={false}; keep existing default behavior tests intact and reference the
SongRow component/conditional action-label rendering so the new branch is
exercised.

In `@frontend/src/pages/AdminPage.test.tsx`:
- Around line 28-126: Add test coverage for the missing delete flows in
AdminPage. The current AdminPage.test.tsx covers user deletion, band listing,
and adding allowed emails, but not the delete actions wired through
useDeleteAdminBand in AdminBandsPanel or useRemoveAllowedEmail in
AdminAccessPolicyPanel. Extend the fetch stub to handle DELETE requests for
/api/admin/bands/:id and /api/admin/access-policy/emails/:id, then add test
cases that open the Bands tab and Access Policy tab, trigger the delete/remove
actions, and assert the expected DELETE calls were made.

In `@frontend/src/pages/AdminPage.tsx`:
- Around line 24-53: The tab switcher in AdminPage is missing the required ARIA
linkage between each tab and its content. Update the tab buttons in AdminPage so
each one has a stable id and aria-controls pointing to its panel, and wrap each
rendered panel (AdminUsersPanel, AdminBandsPanel, AdminAccessPolicyPanel) with
role="tabpanel" and a matching aria-labelledby. Keep the existing tab state
logic, but ensure the tabs and panels are explicitly associated for assistive
technologies.
- Around line 57-176: `AdminUsersPanel` and `AdminBandsPanel` duplicate the same
list rendering and confirm-delete flow, so extract the shared behavior into a
reusable generic component or hook (for example, a common admin entity
list/delete helper) and keep only the entity-specific fields and copy in each
panel. Move the repeated `target` state, empty-state UI, delete button wiring,
error alert, and `ConfirmModal` setup into the shared abstraction, using the
existing `AdminUsersPanel` and `AdminBandsPanel` symbols as the call sites.

In `@internal/handlers/admin_test.go`:
- Around line 134-186: Add negative-path tests in TestAdminAccessPolicy for
malformed request bodies so the c.Bind failure path is covered for both
AdminSetAccessPolicy and AdminAddAllowedEmail. Reuse the existing admin setup
and issue PUT /api/admin/access-policy and POST /api/admin/access-policy/emails
with invalid JSON, then assert each returns HTTP 400; keep the new cases near
the current success-path checks so the coverage stays tied to
AdminSetAccessPolicy and AdminAddAllowedEmail.

In `@internal/handlers/admin.go`:
- Around line 42-48: The AdminUsers handler currently returns the full result
set from Repo.AllUsers with no limit or offset, so update AdminUsers to support
pagination and avoid loading the entire table on every request; use the existing
AdminUsers and AdminBands handlers as the touchpoints, add request-driven
limit/offset (or page/cursor) handling, and thread those values through the
repository call so only a bounded slice is fetched and serialized.

In `@internal/handlers/auth_test.go`:
- Around line 216-272: The signup-related tests are making mutating POST
requests without the required Sec-Fetch-Site header. Update the shared test
helper used by TestSignupAccessPolicy, TestSignupAccessPolicyAllowsAdminEmail,
and TestMeIncludesIsAdmin so that postJSON sends Sec-Fetch-Site: same-origin on
mutating requests, and keep the individual tests using that helper unchanged.

In `@internal/model/accesspolicy.go`:
- Around line 13-18: `AllowedEmail.Email` is only protected by a case-sensitive
unique index, so allow-list matching can break when callers pass differently
cased or padded addresses. Update the repository layer in `AddAllowedEmail` and
`EmailAllowed` to normalize emails consistently before save and lookup (for
example, trim and lowercase the input), so the behavior does not depend on every
caller in the admin and signup paths doing it correctly. Use the `AllowedEmail`
model and the repository methods as the single source of truth for email
normalization.

In `@internal/repository/users.go`:
- Around line 87-92: The user cleanup in the delete transaction only removes
`BandMember` rows and `BandInvite` rows where `invited_user_id` matches, but it
leaves invites created by the user behind. Update the delete logic in the user
repository cleanup flow to also delete `BandInvite` records where `created_by =
?` using the same `userID`, alongside the existing `invited_user_id` removal, so
all references to the deleted user are cleared.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9b3b4daa-d39e-49e2-9845-ffe2c136685c

📥 Commits

Reviewing files that changed from the base of the PR and between aadc6e0 and 0fe257f.

📒 Files selected for processing (48)
  • .gitignore
  • cmd/bandwidth/main.go
  • cmd/bandwidth/server.go
  • docker-compose.yml
  • docs/superpowers/plans/2026-07-03-admin-access-policy.md
  • docs/superpowers/specs/2026-07-03-admin-access-policy-design.md
  • frontend/src/App.tsx
  • frontend/src/components/Footer.test.tsx
  • frontend/src/components/Footer.tsx
  • frontend/src/components/Layout.tsx
  • frontend/src/components/RequireAdmin.test.tsx
  • frontend/src/components/RequireAdmin.tsx
  • frontend/src/components/bands/BandFolderRow.tsx
  • frontend/src/components/bands/BandFolderSidebar.tsx
  • frontend/src/components/bands/BandSongList.tsx
  • frontend/src/components/bands/BandSongRow.tsx
  • frontend/src/components/folders/SortableFolderRow.tsx
  • frontend/src/components/folders/SortableSongList.tsx
  • frontend/src/components/songs/SongRow.tsx
  • frontend/src/hooks/admin.ts
  • frontend/src/lib/types.ts
  • frontend/src/pages/AdminPage.test.tsx
  • frontend/src/pages/AdminPage.tsx
  • frontend/src/pages/HomePage.tsx
  • internal/handlers/account.go
  • internal/handlers/admin.go
  • internal/handlers/admin_test.go
  • internal/handlers/api.go
  • internal/handlers/api_test.go
  • internal/handlers/auth.go
  • internal/handlers/auth_test.go
  • internal/handlers/passwordreset.go
  • internal/handlers/twofa.go
  • internal/middleware/auth.go
  • internal/middleware/auth_test.go
  • internal/model/accesspolicy.go
  • internal/repository/admin.go
  • internal/repository/admin_test.go
  • internal/repository/bandinvites.go
  • internal/repository/bands.go
  • internal/repository/bands_test.go
  • internal/repository/folders.go
  • internal/repository/maintenance.go
  • internal/repository/repository.go
  • internal/repository/repository_test.go
  • internal/repository/songs.go
  • internal/repository/users.go
  • internal/repository/users_test.go
💤 Files with no reviewable changes (2)
  • frontend/src/components/bands/BandFolderRow.tsx
  • frontend/src/components/bands/BandSongRow.tsx

Comment thread cmd/bandwidth/server.go
Comment on lines +1038 to +1071
- `internal/handlers/account.go` — three identical occurrences (lines 20, 59, 102):
```go
return c.JSON(http.StatusOK, userResponse(user))
```
become:
```go
return c.JSON(http.StatusOK, a.userResponse(user))
```

- `internal/handlers/auth.go` — line 86:
```go
return c.JSON(http.StatusCreated, userResponse(user))
```
becomes:
```go
return c.JSON(http.StatusCreated, a.userResponse(user))
```
and line 132:
```go
return c.JSON(http.StatusOK, userResponse(user))
```
becomes:
```go
return c.JSON(http.StatusOK, a.userResponse(user))
```

- `internal/handlers/twofa.go` — line 100:
```go
return c.JSON(http.StatusOK, userResponse(user))
```
becomes:
```go
return c.JSON(http.StatusOK, a.userResponse(user))
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fenced code blocks need surrounding blank lines (MD031).

markdownlint flags each go fence nested in this bullet list for missing blank lines before/after.

📝 Add blank lines around each fence
 - `internal/handlers/account.go` — three identical occurrences (lines 20, 59, 102):
+
   ```go
   return c.JSON(http.StatusOK, userResponse(user))
  • become:
  • return c.JSON(http.StatusOK, a.userResponse(user))
(apply the same pattern to the `auth.go` and `twofa.go` bullets immediately below)
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
- `internal/handlers/account.go` — three identical occurrences (lines 20, 59, 102):
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 1039-1039: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1041-1041: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1043-1043: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1048-1048: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1050-1050: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1052-1052: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1054-1054: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1056-1056: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1058-1058: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1060-1060: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1065-1065: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1067-1067: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1069-1069: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-03-admin-access-policy.md` around lines 1038 -
1071, markdownlint is flagging the fenced go examples inside the bullet list
because they are missing surrounding blank lines. Update the markdown in this
section so each code fence under the account.go, auth.go, and twofa.go bullets
has an empty line before and after it, keeping the same content but formatting
it as a proper fenced block within the list.

Source: Linters/SAST tools

Comment on lines +10 to +24
canEdit = true,
selected,
onSelect,
onRename,
onDelete,
}: {
folder: Folder;
canEdit?: boolean;
selected: boolean;
onSelect: () => void;
onRename: (name: string) => void;
onDelete: () => void;
}) {
const {attributes, listeners, setNodeRef, transform, transition} =
useSortable({id: folder.id});
useSortable({id: folder.id, disabled: !canEdit});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct gating of DnD and edit actions; add canEdit=false test coverage.

Disabling useSortable alongside hiding the grip button and rename/delete controls correctly removes both the affordance and the underlying capability. Consider adding a test asserting these controls are absent and drag is disabled when canEdit={false}.

Also applies to: 49-58, 79-100

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/folders/SortableFolderRow.tsx` around lines 10 - 24,
`SortableFolderRow` currently gates drag-and-drop and edit affordances with
`canEdit`, but add coverage to prove the disabled state is fully enforced.
Update the tests for `SortableFolderRow` to render with `canEdit={false}` and
assert the grip/rename/delete controls are not present and the `useSortable`
interaction is disabled for that row. Use the component’s
`SortableFolderRow`/`useSortable` behavior in the test setup so the missing
affordances and disabled drag state are verified together.

Comment on lines +5 to +10
<a
href="https://github.com/jwhumphries"
target="_blank"
rel="noopener noreferrer"
className="text-base-content/30 hover:text-base-content/60 fixed right-3 bottom-2 z-10 text-xs transition-colors"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Verify fixed-position footer doesn't overlap other bottom-right UI.

Global fixed positioning at right-3 bottom-2 with z-10 renders on every page (App.tsx mounts it above the route tree per the App.tsx snippet). Worth double-checking it doesn't overlap toasts, floating buttons, or other bottom-right anchored controls on pages like SongPage/BandPage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/Footer.tsx` around lines 5 - 10, The Footer link uses
global fixed positioning with bottom-right anchoring and a z-index, which can
overlap other UI in pages that also place controls there. Update Footer so its
placement is conditional or offset-aware, and verify it does not collide with
bottom-right elements rendered by routes like SongPage and BandPage. Use the
Footer component and its fixed positioning styles as the main place to adjust
behavior.

Comment on lines +9 to +22
linkTo,
onPracticed,
canEdit = true,
actionLabel = 'Practiced',
}: {
song: SongListItem;
linkTo: string;
onPracticed: (id: number, date: string) => void;
canEdit?: boolean;
actionLabel?: string;
}) {
return (
<li className="group border-base-300/60 bg-base-100 hover:border-base-300 flex items-center gap-3 rounded-box border p-3 transition-all hover:shadow-md sm:gap-4 sm:p-4">
<Link to={`/songs/${song.id}`} className="min-w-0 flex-1">
<Link to={linkTo} className="min-w-0 flex-1">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Solid prop extension; add coverage for canEdit=false.

Defaults preserve existing behavior for all current callers, and the conditional gating logic is correct. Since this is a new branch (hidden action button), consider adding a test asserting the button is not rendered when canEdit={false}.

Also applies to: 43-52

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/songs/SongRow.tsx` around lines 9 - 22, Add test
coverage for the new hidden-action branch in SongRow by asserting the action
button is not rendered when SongRow is passed canEdit={false}; keep existing
default behavior tests intact and reference the SongRow component/conditional
action-label rendering so the new branch is exercised.

Comment thread internal/handlers/admin_test.go
Comment on lines +42 to +48
func (a *API) AdminUsers(c *echo.Context) error {
users, err := a.Repo.AllUsers()
if err != nil {
return err
}
return c.JSON(http.StatusOK, users)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial

Consider pagination for unbounded admin listings.

AllUsers/AllBands are returned in full with no limit/offset. Fine at current scale, but worth revisiting if the user/band count grows meaningfully, since the whole table is loaded and serialized on every admin panel visit.

Also applies to: 75-81

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handlers/admin.go` around lines 42 - 48, The AdminUsers handler
currently returns the full result set from Repo.AllUsers with no limit or
offset, so update AdminUsers to support pagination and avoid loading the entire
table on every request; use the existing AdminUsers and AdminBands handlers as
the touchpoints, add request-driven limit/offset (or page/cursor) handling, and
thread those values through the repository call so only a bounded slice is
fetched and serialized.

Comment on lines +216 to +272
func TestSignupAccessPolicy(t *testing.T) {
e, api := newTestAPI(t)
if err := api.Repo.SetAccessPolicyEnabled(true); err != nil {
t.Fatal(err)
}

rec := postJSON(e, "/api/auth/signup",
`{"username":"outsider","email":"outsider@example.com","password":"hunter2hunter2"}`)
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want 403", rec.Code)
}

admin, _ := api.Repo.CreateUser("admin", "admin@example.com", "h")
if _, err := api.Repo.AddAllowedEmail("friend@example.com", admin.ID); err != nil {
t.Fatal(err)
}
rec = postJSON(e, "/api/auth/signup",
`{"username":"friend","email":"friend@example.com","password":"hunter2hunter2"}`)
if rec.Code != http.StatusCreated {
t.Fatalf("allow-listed signup: %d %s", rec.Code, rec.Body.String())
}
}

func TestSignupAccessPolicyAllowsAdminEmail(t *testing.T) {
e, api := newTestAPI(t)
api.AdminEmails = map[string]bool{"admin@example.com": true}
if err := api.Repo.SetAccessPolicyEnabled(true); err != nil {
t.Fatal(err)
}

rec := postJSON(e, "/api/auth/signup",
`{"username":"admin","email":"admin@example.com","password":"hunter2hunter2"}`)
if rec.Code != http.StatusCreated {
t.Fatalf("admin signup: %d %s", rec.Code, rec.Body.String())
}
}

func TestMeIncludesIsAdmin(t *testing.T) {
e, api := newTestAPI(t)
api.AdminEmails = map[string]bool{"alice@example.com": true}
rec := postJSON(e, "/api/auth/signup",
`{"username":"alice","email":"alice@example.com","password":"hunter2hunter2"}`)
cookie := sessionCookie(t, rec)

req := httptest.NewRequest(http.MethodGet, "/api/me", nil)
req.AddCookie(cookie)
mrec := httptest.NewRecorder()
e.ServeHTTP(mrec, req)

var body map[string]any
if err := json.Unmarshal(mrec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if body["isAdmin"] != true {
t.Errorf("isAdmin = %v, want true", body["isAdmin"])
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Set Sec-Fetch-Site: same-origin on the new mutating signup requests.

TestSignupAccessPolicy, TestSignupAccessPolicyAllowsAdminEmail, and TestMeIncludesIsAdmin all POST to /api/auth/signup via postJSON, which doesn't set Sec-Fetch-Site. As per coding guidelines, "Tests must set Sec-Fetch-Site: same-origin on mutating requests."

🔧 Proposed fix (update shared helper)
 func postJSON(e *echo.Echo, path, body string, cookies ...*http.Cookie) *httptest.ResponseRecorder {
 	req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
 	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Sec-Fetch-Site", "same-origin")
 	for _, c := range cookies {
 		req.AddCookie(c)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handlers/auth_test.go` around lines 216 - 272, The signup-related
tests are making mutating POST requests without the required Sec-Fetch-Site
header. Update the shared test helper used by TestSignupAccessPolicy,
TestSignupAccessPolicyAllowsAdminEmail, and TestMeIncludesIsAdmin so that
postJSON sends Sec-Fetch-Site: same-origin on mutating requests, and keep the
individual tests using that helper unchanged.

Source: Coding guidelines

Comment thread internal/model/accesspolicy.go
Comment thread internal/repository/users.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/repository/admin.go (1)

63-79: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Enforce the access-policy singleton at the database level. Two concurrent first-time callers can still create duplicate rows here, since the table only has a plain primary key and no uniqueness guard for the singleton record. A fixed ID or ON CONFLICT DO NOTHING/FirstOrCreate would make this invariant safe.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/repository/admin.go` around lines 63 - 79, The accessPolicy
singleton is still race-prone because Repo.accessPolicy can create duplicate
rows when two first-time callers hit the ErrRecordNotFound path together. Update
accessPolicy to enforce the singleton at the database level by using a fixed
primary key or an atomic create path such as FirstOrCreate/ON CONFLICT DO
NOTHING, so only one AccessPolicy row can ever be inserted even under concurrent
access.
♻️ Duplicate comments (2)
internal/handlers/auth_test.go (1)

37-46: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

postJSON still missing Sec-Fetch-Site: same-origin on mutating requests.

New tests (TestSignupAccessPolicy, TestSignupAccessPolicyAllowsAdminEmail, TestMeIncludesIsAdmin) POST to /api/auth/signup via this helper without the header. As per coding guidelines, "Tests must set Sec-Fetch-Site: same-origin on mutating requests."

🔧 Proposed fix
 func postJSON(e *echo.Echo, path, body string, cookies ...*http.Cookie) *httptest.ResponseRecorder {
 	req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
 	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Sec-Fetch-Site", "same-origin")
 	for _, c := range cookies {
 		req.AddCookie(c)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handlers/auth_test.go` around lines 37 - 46, The shared test helper
postJSON in auth_test.go is missing the required Sec-Fetch-Site header for
mutating requests. Update postJSON so every POST request it builds includes
Sec-Fetch-Site set to same-origin before calling e.ServeHTTP, ensuring tests
like TestSignupAccessPolicy, TestSignupAccessPolicyAllowsAdminEmail, and
TestMeIncludesIsAdmin use the correct access policy context.

Source: Coding guidelines

frontend/src/pages/AdminPage.tsx (1)

87-206: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate list/confirm-delete pattern still unresolved.

AdminUsersPanel and AdminBandsPanel still repeat the same target state, empty-state block, delete button wiring, error alert, and ConfirmModal usage, differing only in field names/copy. This was flagged previously without an "addressed" resolution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/AdminPage.tsx` around lines 87 - 206, The duplicate
list-and-confirm-delete flow still exists in AdminUsersPanel and
AdminBandsPanel, with the same target state, empty-state UI, delete button
handler, error alert, and ConfirmModal wiring repeated. Refactor this shared
pattern into a reusable component or helper that accepts the specific item list,
labels, delete mutation hook, and display fields, and update AdminUsersPanel and
AdminBandsPanel to use it while preserving their different copy and
item-specific properties.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/repository/admin.go`:
- Around line 63-79: The accessPolicy singleton is still race-prone because
Repo.accessPolicy can create duplicate rows when two first-time callers hit the
ErrRecordNotFound path together. Update accessPolicy to enforce the singleton at
the database level by using a fixed primary key or an atomic create path such as
FirstOrCreate/ON CONFLICT DO NOTHING, so only one AccessPolicy row can ever be
inserted even under concurrent access.

---

Duplicate comments:
In `@frontend/src/pages/AdminPage.tsx`:
- Around line 87-206: The duplicate list-and-confirm-delete flow still exists in
AdminUsersPanel and AdminBandsPanel, with the same target state, empty-state UI,
delete button handler, error alert, and ConfirmModal wiring repeated. Refactor
this shared pattern into a reusable component or helper that accepts the
specific item list, labels, delete mutation hook, and display fields, and update
AdminUsersPanel and AdminBandsPanel to use it while preserving their different
copy and item-specific properties.

In `@internal/handlers/auth_test.go`:
- Around line 37-46: The shared test helper postJSON in auth_test.go is missing
the required Sec-Fetch-Site header for mutating requests. Update postJSON so
every POST request it builds includes Sec-Fetch-Site set to same-origin before
calling e.ServeHTTP, ensuring tests like TestSignupAccessPolicy,
TestSignupAccessPolicyAllowsAdminEmail, and TestMeIncludesIsAdmin use the
correct access policy context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8dd280b2-bbf2-4508-83bc-c6eb5fd53729

📥 Commits

Reviewing files that changed from the base of the PR and between 0fe257f and 5e73c93.

📒 Files selected for processing (13)
  • cmd/bandwidth/server_test.go
  • docs/superpowers/plans/2026-07-03-admin-access-policy.md
  • frontend/src/components/folders/SortableFolderRow.test.tsx
  • frontend/src/components/songs/SongRow.test.tsx
  • frontend/src/pages/AdminPage.test.tsx
  • frontend/src/pages/AdminPage.tsx
  • frontend/src/pages/JoinPage.test.tsx
  • frontend/src/pages/JoinPage.tsx
  • internal/handlers/admin_test.go
  • internal/handlers/auth.go
  • internal/handlers/auth_test.go
  • internal/repository/admin.go
  • internal/repository/users.go

@jwhumphries jwhumphries merged commit 0bcf039 into main Jul 5, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant