Admin Dashboard Overview

Admin UI for offer review, user management, and platform oversight

What is the Admin Dashboard?

The Admin Dashboard is a set of pages under /admin/* in zooly-app that gives administrators the ability to:

  1. Review offers in the bouncer queue (approve/reject before talent sees them)
  2. View all offers across the platform with status filtering and pagination
  3. Manage users — search accounts, block/unblock users

All admin pages require the admin role. Unauthorized users see a 403 page.

Architecture

Layout and Auth

Layout: apps/zooly-app/app/(pages)/admin/layout.tsx

The admin layout wraps all /admin/* pages and provides:

  • Role check on mount: probes GET /api/offers/admin/queue?limit=1 — if the response is not 403, the user is authorized. Shows a loading state while checking.
  • Navigation bar: tabs for Bouncer Queue, All Offers, and Users
  • 403 fallback: renders "Admin access required" for unauthorized users

The admin role itself is verified server-side by assertAdmin() from @zooly/util-srv, which resolves the user from the auth cookie and checks for the admin role. All admin API endpoints use assertAdmin().

Pages

All admin pages are client components that fetch data on mount and provide interactive controls.

PageRouteFile
Bouncer Queue/admin/offers/queueapps/zooly-app/app/(pages)/admin/offers/queue/page.tsx
All Offers/admin/offersapps/zooly-app/app/(pages)/admin/offers/page.tsx
User Management/admin/usersapps/zooly-app/app/(pages)/admin/users/page.tsx

Bouncer Queue

Displays offers in ADMIN_REVIEW status — every new offer and every counter-offer passes through this queue before the other party can see it.

Data source: GET /api/offers/admin/queue (existing endpoint from Sprint 1)

Features:

  • Shows offer title, buyer name, talent name, amount, and time since submission
  • High Value badge on offers where totalBrandPriceCent >= 500000 ($5,000+)
  • Approve button — calls POST /api/offers/admin/approve with { offerId }
  • Reject button — expands a textarea for rejection reason, then calls POST /api/offers/admin/reject with { offerId, reason }
  • Auto-refreshes the list after each action

Related API endpoints: see existing offer admin routes in apps/zooly-app/app/api/offers/admin/


All Offers

Displays a paginated table of all offers on the platform.

Data source: GET /api/offers/list?perspective=admin (existing endpoint)

Features:

  • Status filter dropdown with all 12 offer statuses
  • Paginated table showing: offer title, status, amount, buyer/seller names, creation date
  • Previous / Next pagination controls
  • Total count display

User Management

Search, list, and block/unblock user accounts.

Data source: GET /api/admin/users and POST /api/admin/users

Features:

  • Search by display name or slug (calls with ?search= query param)
  • Paginated table showing: display name, slug, creation date, blocked status
  • Block button — prompts for a reason, calls POST /api/admin/users with { accountId, action: "block", reason }
  • Unblock button — calls POST /api/admin/users with { accountId, action: "unblock" }

Blocking Implementation

Account blocking is implemented via the brand_limits table (not a column on account):

  • blockAccount(accountId, reason, adminAccountId) — upserts into brand_limits setting isBlocked = true
  • unblockAccount(accountId, adminAccountId) — sets isBlocked = false
  • Both functions are in packages/db/src/access/brandLimits.ts
  • Blocked brands cannot create offers (checked by checkBrandCanCreateOffer)

Account Listing

The listAccounts(limit, offset, search?) function in packages/db/src/access/account.ts supports:

  • Optional search parameter that filters by displayName or slug using case-insensitive ilike
  • Returns { accounts: Account[], total: number } for pagination

Adding New Admin Pages

To add a new admin page:

  1. Create a page component in apps/zooly-app/app/(pages)/admin/<section>/page.tsx
  2. It will automatically inherit the admin layout and role check
  3. Add a navigation link to NAV_ITEMS in apps/zooly-app/app/(pages)/admin/layout.tsx
  4. For the backend, use assertAdmin(cookieHeader) from @zooly/util-srv in your API route