Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .claude/memory/iterations.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,32 @@ _Append only. One entry per session or PR. Never delete._

---

## 2026-03-21 — Phase 3: Auth + Superadmin + Polish (revamp/phase-3)

### What changed
- **Real Clerk auth wired**: deleted `lib/auth-helpers.ts` (hardcoded stubs). All 9 API routes now call `requireAgencyAuth()` / `requireCreatorAuth()` / `requireBrandAuth()` from `lib/auth.ts`.
- **ClerkProvider**: `app/layout.tsx` now wraps with `<ClerkProvider>` instead of `<RoleProvider>`.
- **Deleted `lib/role-context.tsx`**: Clerk is now source of truth for identity.
- **RoleSwitcher rewrite**: uses `useUser()` from Clerk; renders null for non-superadmin; sets `active_perspective` cookie on change.
- **Header**: replaced `useRole()` with `useUser()` from Clerk.
- **Rate limiting**: `lib/rate-limit.ts` rewritten with lazy singletons (`getAuthRateLimit()`, `getUploadRateLimit()`) to prevent build-time crash when Upstash env vars absent.
- **Resend lazy init**: `jobs/send-email.ts` uses `getResend()` singleton for same reason.
- **8 email templates**: `changes-requested`, `content-approved`, `payment-received`, `deadline-warning`, `partnership-request`, `partnership-accepted`, `partnership-declined`, `new-brief`.
- **Email triggers**: fire-and-forget `sendEmailJob.trigger()` wired to 10 API events across deals, submissions, partnerships, briefs.
- **Upload rate limiting**: `app/api/v1/deals/[id]/submissions/route.ts` POST now checks `getUploadRateLimit()`.
- **Auth pages**: `/login`, `/signup`, `/signup/agency`, `/signup/creator`, `/signup/brand`, `/signup/complete`.
- **Loading/error boundaries**: `loading.tsx` + `error.tsx` added to `(agency)`, `(creator)`, `(brand)` route groups.
- **Empty states**: dashboard, deals, roster, brands, briefs pages.
- **`proxy.ts` fix**: added `/api/v1/creators(.*)` to `isPublicRoute` (was causing /discover 500).
- **`serverFetch()` helper**: added to `lib/api.ts`; all protected server components now forward Clerk session cookie on internal HTTP fetches, fixing `SyntaxError: Unexpected token '<'` on all agency/creator pages.
- **Smoke tests**: `e2e/smoke.spec.ts` (3 flows) + `e2e/helpers/auth.ts`.
- **CI/CD**: Clerk env vars added to `.github/workflows/ci.yml` and `prod.yml`.

### Why
Phase 3 milestone — activate real auth replacing hardcoded test stubs, add superadmin role, polish UI, wire email notifications.

---

## 2026-03-21 — revamp/phase-3: Auth + Superadmin + Email + Polish
**Type**: Feature
**Branch**: revamp/phase-3
Expand Down
4 changes: 2 additions & 2 deletions app/(agency)/brands/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Expand Down Expand Up @@ -36,7 +36,7 @@ export default async function BrandDetailPage({
params: Promise<{ id: string }>
}) {
const { id } = await params
const res = await fetch(apiUrl(`/api/v1/brands/${id}`), { cache: 'no-store' })
const res = await serverFetch(`/api/v1/brands/${id}`, { cache: 'no-store' })
if (!res.ok) notFound()
const { data: brand } = await res.json()
if (!brand) notFound()
Expand Down
6 changes: 3 additions & 3 deletions app/(agency)/brands/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { BrandsTable } from '@/components/brands/BrandsTable'

export default async function BrandsPage() {
const [brandsRes, dealsRes] = await Promise.all([
fetch(apiUrl('/api/v1/brands'), { cache: 'no-store' }),
fetch(apiUrl('/api/v1/deals'), { cache: 'no-store' }),
serverFetch('/api/v1/brands', { cache: 'no-store' }),
serverFetch('/api/v1/deals', { cache: 'no-store' }),
])
const { data: brands } = await brandsRes.json()
const { data: deals } = await dealsRes.json()
Expand Down
4 changes: 2 additions & 2 deletions app/(agency)/briefs/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { notFound } from 'next/navigation'
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { BriefDetail } from '@/components/briefs/BriefDetail'

export default async function BriefDetailPage({
Expand All @@ -8,7 +8,7 @@ export default async function BriefDetailPage({
params: Promise<{ id: string }>
}) {
const { id } = await params
const res = await fetch(apiUrl(`/api/v1/briefs/${id}`), { cache: 'no-store' })
const res = await serverFetch(`/api/v1/briefs/${id}`, { cache: 'no-store' })
if (!res.ok) notFound()
const { data: brief } = await res.json()
if (!brief) notFound()
Expand Down
4 changes: 2 additions & 2 deletions app/(agency)/briefs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { BriefsTable } from '@/components/briefs/BriefsTable'

export default async function BriefsPage() {
const res = await fetch(apiUrl('/api/v1/briefs'), { cache: 'no-store' })
const res = await serverFetch('/api/v1/briefs', { cache: 'no-store' })
const { data: briefs } = await res.json()

if ((briefs ?? []).length === 0) {
Expand Down
4 changes: 2 additions & 2 deletions app/(agency)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Link from 'next/link'
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { KanbanBoard } from '@/components/kanban/KanbanBoard'

export default async function DashboardPage() {
const res = await fetch(apiUrl('/api/v1/deals'), { cache: 'no-store' })
const res = await serverFetch('/api/v1/deals', { cache: 'no-store' })
const { data: deals } = await res.json()

if ((deals ?? []).length === 0) {
Expand Down
4 changes: 2 additions & 2 deletions app/(agency)/deals/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { notFound } from 'next/navigation'
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { DealDetail } from '@/components/deals/DealDetail'

export default async function DealDetailPage({
Expand All @@ -9,7 +9,7 @@ export default async function DealDetailPage({
}) {
const { id } = await params

const res = await fetch(apiUrl(`/api/v1/deals/${id}`), { cache: 'no-store' })
const res = await serverFetch(`/api/v1/deals/${id}`, { cache: 'no-store' })
if (!res.ok) notFound()
const { data: deal } = await res.json()
if (!deal) notFound()
Expand Down
6 changes: 3 additions & 3 deletions app/(agency)/deals/new/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { DealNewForm } from '@/components/forms/DealNewForm'

export default async function NewDealPage() {
const [brandsRes, rosterRes] = await Promise.all([
fetch(apiUrl('/api/v1/brands'), { cache: 'no-store' }),
fetch(apiUrl('/api/v1/roster'), { cache: 'no-store' }),
serverFetch('/api/v1/brands', { cache: 'no-store' }),
serverFetch('/api/v1/roster', { cache: 'no-store' }),
])
const { data: brands } = await brandsRes.json()
const { data: creators } = await rosterRes.json()
Expand Down
4 changes: 2 additions & 2 deletions app/(agency)/deals/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { DealsTable } from '@/components/deals/DealsTable'

export default async function DealsPage() {
const res = await fetch(apiUrl('/api/v1/deals'), { cache: 'no-store' })
const res = await serverFetch('/api/v1/deals', { cache: 'no-store' })
const { data: deals } = await res.json()

if ((deals ?? []).length === 0) {
Expand Down
6 changes: 3 additions & 3 deletions app/(agency)/roster/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { RosterTable } from '@/components/roster/RosterTable'

export default async function RosterPage() {
const [rosterRes, dealsRes] = await Promise.all([
fetch(apiUrl('/api/v1/roster'), { cache: 'no-store' }),
fetch(apiUrl('/api/v1/deals'), { cache: 'no-store' }),
serverFetch('/api/v1/roster', { cache: 'no-store' }),
serverFetch('/api/v1/deals', { cache: 'no-store' }),
])
const { data: creators } = await rosterRes.json()
const { data: deals } = await dealsRes.json()
Expand Down
4 changes: 2 additions & 2 deletions app/(creator)/creator/deals/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { notFound } from 'next/navigation'
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { CreatorDealDetail } from '@/components/creator/CreatorDealDetail'

export default async function CreatorDealDetailPage({
Expand All @@ -8,7 +8,7 @@ export default async function CreatorDealDetailPage({
params: Promise<{ id: string }>
}) {
const { id } = await params
const res = await fetch(apiUrl(`/api/v1/deals/${id}`), { cache: 'no-store' })
const res = await serverFetch(`/api/v1/deals/${id}`, { cache: 'no-store' })
if (!res.ok) notFound()
const { data: deal } = await res.json()
if (!deal) notFound()
Expand Down
4 changes: 2 additions & 2 deletions app/(creator)/creator/deals/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { apiUrl } from '@/lib/api'
import { serverFetch } from '@/lib/api'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import Link from 'next/link'
Expand All @@ -16,7 +16,7 @@ const STAGE_LABELS: Record<string, string> = {
}

export default async function CreatorDealsPage() {
const res = await fetch(apiUrl('/api/v1/deals?creatorId=creator_seed_001'), { cache: 'no-store' })
const res = await serverFetch('/api/v1/deals', { cache: 'no-store' })
const { data } = await res.json()
const myDeals = data ?? []

Expand Down
14 changes: 14 additions & 0 deletions lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import { cookies } from 'next/headers'

export const API_BASE = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3001'

export const apiUrl = (path: string) => `${API_BASE}${path}`

/** Server-side fetch that forwards the Clerk session cookie so protected API routes don't redirect to /login. */
export async function serverFetch(path: string, init?: RequestInit): Promise<Response> {
const cookieStore = await cookies()
return fetch(apiUrl(path), {
...init,
headers: {
...(init?.headers as Record<string, string> | undefined),
cookie: cookieStore.toString(),
},
})
}
Loading