Skip to content

Commit b0701d4

Browse files
parva3105claude
andauthored
feat(M3): Phase 3 — Clerk auth, superadmin, email templates, polish
Add serverFetch() helper to lib/api.ts that reads cookies() from next/headers and forwards them on every internal API call. All 11 protected server components (agency + creator pages) now use serverFetch() instead of bare fetch(apiUrl()), preventing the Clerk middleware from redirecting requests to /login and returning HTML instead of JSON. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 46f1ade commit b0701d4

13 files changed

Lines changed: 65 additions & 25 deletions

File tree

.claude/memory/iterations.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,32 @@ _Append only. One entry per session or PR. Never delete._
33

44
---
55

6+
## 2026-03-21 — Phase 3: Auth + Superadmin + Polish (revamp/phase-3)
7+
8+
### What changed
9+
- **Real Clerk auth wired**: deleted `lib/auth-helpers.ts` (hardcoded stubs). All 9 API routes now call `requireAgencyAuth()` / `requireCreatorAuth()` / `requireBrandAuth()` from `lib/auth.ts`.
10+
- **ClerkProvider**: `app/layout.tsx` now wraps with `<ClerkProvider>` instead of `<RoleProvider>`.
11+
- **Deleted `lib/role-context.tsx`**: Clerk is now source of truth for identity.
12+
- **RoleSwitcher rewrite**: uses `useUser()` from Clerk; renders null for non-superadmin; sets `active_perspective` cookie on change.
13+
- **Header**: replaced `useRole()` with `useUser()` from Clerk.
14+
- **Rate limiting**: `lib/rate-limit.ts` rewritten with lazy singletons (`getAuthRateLimit()`, `getUploadRateLimit()`) to prevent build-time crash when Upstash env vars absent.
15+
- **Resend lazy init**: `jobs/send-email.ts` uses `getResend()` singleton for same reason.
16+
- **8 email templates**: `changes-requested`, `content-approved`, `payment-received`, `deadline-warning`, `partnership-request`, `partnership-accepted`, `partnership-declined`, `new-brief`.
17+
- **Email triggers**: fire-and-forget `sendEmailJob.trigger()` wired to 10 API events across deals, submissions, partnerships, briefs.
18+
- **Upload rate limiting**: `app/api/v1/deals/[id]/submissions/route.ts` POST now checks `getUploadRateLimit()`.
19+
- **Auth pages**: `/login`, `/signup`, `/signup/agency`, `/signup/creator`, `/signup/brand`, `/signup/complete`.
20+
- **Loading/error boundaries**: `loading.tsx` + `error.tsx` added to `(agency)`, `(creator)`, `(brand)` route groups.
21+
- **Empty states**: dashboard, deals, roster, brands, briefs pages.
22+
- **`proxy.ts` fix**: added `/api/v1/creators(.*)` to `isPublicRoute` (was causing /discover 500).
23+
- **`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.
24+
- **Smoke tests**: `e2e/smoke.spec.ts` (3 flows) + `e2e/helpers/auth.ts`.
25+
- **CI/CD**: Clerk env vars added to `.github/workflows/ci.yml` and `prod.yml`.
26+
27+
### Why
28+
Phase 3 milestone — activate real auth replacing hardcoded test stubs, add superadmin role, polish UI, wire email notifications.
29+
30+
---
31+
632
## 2026-03-21 — revamp/phase-3: Auth + Superadmin + Email + Polish
733
**Type**: Feature
834
**Branch**: revamp/phase-3

app/(agency)/brands/[id]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { notFound } from 'next/navigation'
22
import Link from 'next/link'
3-
import { apiUrl } from '@/lib/api'
3+
import { serverFetch } from '@/lib/api'
44
import { Badge } from '@/components/ui/badge'
55
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
66
import {
@@ -36,7 +36,7 @@ export default async function BrandDetailPage({
3636
params: Promise<{ id: string }>
3737
}) {
3838
const { id } = await params
39-
const res = await fetch(apiUrl(`/api/v1/brands/${id}`), { cache: 'no-store' })
39+
const res = await serverFetch(`/api/v1/brands/${id}`, { cache: 'no-store' })
4040
if (!res.ok) notFound()
4141
const { data: brand } = await res.json()
4242
if (!brand) notFound()

app/(agency)/brands/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { apiUrl } from '@/lib/api'
1+
import { serverFetch } from '@/lib/api'
22
import { BrandsTable } from '@/components/brands/BrandsTable'
33

44
export default async function BrandsPage() {
55
const [brandsRes, dealsRes] = await Promise.all([
6-
fetch(apiUrl('/api/v1/brands'), { cache: 'no-store' }),
7-
fetch(apiUrl('/api/v1/deals'), { cache: 'no-store' }),
6+
serverFetch('/api/v1/brands', { cache: 'no-store' }),
7+
serverFetch('/api/v1/deals', { cache: 'no-store' }),
88
])
99
const { data: brands } = await brandsRes.json()
1010
const { data: deals } = await dealsRes.json()

app/(agency)/briefs/[id]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { notFound } from 'next/navigation'
2-
import { apiUrl } from '@/lib/api'
2+
import { serverFetch } from '@/lib/api'
33
import { BriefDetail } from '@/components/briefs/BriefDetail'
44

55
export default async function BriefDetailPage({
@@ -8,7 +8,7 @@ export default async function BriefDetailPage({
88
params: Promise<{ id: string }>
99
}) {
1010
const { id } = await params
11-
const res = await fetch(apiUrl(`/api/v1/briefs/${id}`), { cache: 'no-store' })
11+
const res = await serverFetch(`/api/v1/briefs/${id}`, { cache: 'no-store' })
1212
if (!res.ok) notFound()
1313
const { data: brief } = await res.json()
1414
if (!brief) notFound()

app/(agency)/briefs/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { apiUrl } from '@/lib/api'
1+
import { serverFetch } from '@/lib/api'
22
import { BriefsTable } from '@/components/briefs/BriefsTable'
33

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

88
if ((briefs ?? []).length === 0) {

app/(agency)/dashboard/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import Link from 'next/link'
2-
import { apiUrl } from '@/lib/api'
2+
import { serverFetch } from '@/lib/api'
33
import { KanbanBoard } from '@/components/kanban/KanbanBoard'
44

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

99
if ((deals ?? []).length === 0) {

app/(agency)/deals/[id]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { notFound } from 'next/navigation'
2-
import { apiUrl } from '@/lib/api'
2+
import { serverFetch } from '@/lib/api'
33
import { DealDetail } from '@/components/deals/DealDetail'
44

55
export default async function DealDetailPage({
@@ -9,7 +9,7 @@ export default async function DealDetailPage({
99
}) {
1010
const { id } = await params
1111

12-
const res = await fetch(apiUrl(`/api/v1/deals/${id}`), { cache: 'no-store' })
12+
const res = await serverFetch(`/api/v1/deals/${id}`, { cache: 'no-store' })
1313
if (!res.ok) notFound()
1414
const { data: deal } = await res.json()
1515
if (!deal) notFound()

app/(agency)/deals/new/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { apiUrl } from '@/lib/api'
1+
import { serverFetch } from '@/lib/api'
22
import { DealNewForm } from '@/components/forms/DealNewForm'
33

44
export default async function NewDealPage() {
55
const [brandsRes, rosterRes] = await Promise.all([
6-
fetch(apiUrl('/api/v1/brands'), { cache: 'no-store' }),
7-
fetch(apiUrl('/api/v1/roster'), { cache: 'no-store' }),
6+
serverFetch('/api/v1/brands', { cache: 'no-store' }),
7+
serverFetch('/api/v1/roster', { cache: 'no-store' }),
88
])
99
const { data: brands } = await brandsRes.json()
1010
const { data: creators } = await rosterRes.json()

app/(agency)/deals/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { apiUrl } from '@/lib/api'
1+
import { serverFetch } from '@/lib/api'
22
import { DealsTable } from '@/components/deals/DealsTable'
33

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

88
if ((deals ?? []).length === 0) {

app/(agency)/roster/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { apiUrl } from '@/lib/api'
1+
import { serverFetch } from '@/lib/api'
22
import { RosterTable } from '@/components/roster/RosterTable'
33

44
export default async function RosterPage() {
55
const [rosterRes, dealsRes] = await Promise.all([
6-
fetch(apiUrl('/api/v1/roster'), { cache: 'no-store' }),
7-
fetch(apiUrl('/api/v1/deals'), { cache: 'no-store' }),
6+
serverFetch('/api/v1/roster', { cache: 'no-store' }),
7+
serverFetch('/api/v1/deals', { cache: 'no-store' }),
88
])
99
const { data: creators } = await rosterRes.json()
1010
const { data: deals } = await dealsRes.json()

0 commit comments

Comments
 (0)