This commit is contained in:
Waleed Latif
2026-02-11 12:09:23 -08:00
parent eedf67013c
commit fb1e03e71e
6 changed files with 56 additions and 43 deletions

View File

@@ -1,33 +0,0 @@
'use client'
import { useEffect } from 'react'
import { useSearchParams } from 'next/navigation'
const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content'] as const
const COOKIE_NAME = 'sim_utm'
const COOKIE_MAX_AGE = 3600 // 1 hour
export function UtmCookieSetter() {
const searchParams = useSearchParams()
useEffect(() => {
const hasUtm = UTM_KEYS.some((key) => searchParams.get(key))
if (!hasUtm) return
const utmData: Record<string, string> = {}
for (const key of UTM_KEYS) {
const value = searchParams.get(key)
if (value) {
utmData[key] = value
}
}
utmData.referrer_url = document.referrer || ''
utmData.landing_page = window.location.pathname
utmData.created_at = Date.now().toString()
document.cookie = `${COOKIE_NAME}=${encodeURIComponent(JSON.stringify(utmData))}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`
}, [searchParams])
return null
}

View File

@@ -1,8 +1,7 @@
'use client'
import { Suspense, useEffect } from 'react'
import { useEffect } from 'react'
import AuthBackground from '@/app/(auth)/components/auth-background'
import { UtmCookieSetter } from '@/app/(auth)/components/utm-cookie-setter'
import Nav from '@/app/(landing)/components/nav/nav'
// Helper to detect if a color is dark
@@ -29,9 +28,6 @@ export default function AuthLayout({ children }: { children: React.ReactNode })
}, [])
return (
<AuthBackground>
<Suspense>
<UtmCookieSetter />
</Suspense>
<main className='relative flex min-h-screen flex-col text-foreground'>
{/* Header - Nav handles all conditional logic */}
<Nav hideAuthButtons={true} variant='auth' />

View File

@@ -87,7 +87,15 @@ export async function POST() {
let utmData: Record<string, string>
try {
utmData = JSON.parse(decodeURIComponent(utmCookie.value))
// Next.js encodes cookie values; decode first, falling back to raw
// value if UTM params contain bare % characters (e.g. "50%off")
let decoded: string
try {
decoded = decodeURIComponent(utmCookie.value)
} catch {
decoded = utmCookie.value
}
utmData = JSON.parse(decoded)
} catch {
logger.warn('Failed to parse UTM cookie', { userId: session.user.id })
cookieStore.delete(COOKIE_NAME)

View File

@@ -85,8 +85,13 @@ export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) =
}
if (body.code !== undefined) {
if (body.code !== null && typeof body.code !== 'string') {
return badRequestResponse('code must be a string or null')
if (body.code !== null) {
if (typeof body.code !== 'string') {
return badRequestResponse('code must be a string or null')
}
if (body.code.trim().length < 6) {
return badRequestResponse('code must be at least 6 characters')
}
}
updates.code = body.code ? body.code.trim().toUpperCase() : null
}

View File

@@ -69,8 +69,13 @@ export const POST = withAdminAuth(async (request) => {
return badRequestResponse('bonusCreditAmount must be a positive number')
}
if (code !== undefined && code !== null && typeof code !== 'string') {
return badRequestResponse('code must be a string or null')
if (code !== undefined && code !== null) {
if (typeof code !== 'string') {
return badRequestResponse('code must be a string or null')
}
if (code.trim().length < 6) {
return badRequestResponse('code must be at least 6 characters')
}
}
const id = nanoid()

View File

@@ -137,6 +137,37 @@ function handleSecurityFiltering(request: NextRequest): NextResponse | null {
return null
}
const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content'] as const
const UTM_COOKIE_NAME = 'sim_utm'
const UTM_COOKIE_MAX_AGE = 3600 // 1 hour
/**
* Sets a `sim_utm` cookie when UTM params are present on auth pages.
* Captures UTM values, the HTTP Referer, landing page, and a timestamp
* used by the attribution API to verify the user signed up after visiting the link.
*/
function setUtmCookie(request: NextRequest, response: NextResponse): void {
const { searchParams, pathname } = request.nextUrl
const hasUtm = UTM_KEYS.some((key) => searchParams.get(key))
if (!hasUtm) return
const utmData: Record<string, string> = {}
for (const key of UTM_KEYS) {
const value = searchParams.get(key)
if (value) utmData[key] = value
}
utmData.referrer_url = request.headers.get('referer') || ''
utmData.landing_page = pathname
utmData.created_at = Date.now().toString()
response.cookies.set(UTM_COOKIE_NAME, JSON.stringify(utmData), {
path: '/',
maxAge: UTM_COOKIE_MAX_AGE,
sameSite: 'lax',
httpOnly: false, // Client-side hook needs to detect cookie presence
})
}
export async function proxy(request: NextRequest) {
const url = request.nextUrl
@@ -152,6 +183,7 @@ export async function proxy(request: NextRequest) {
}
const response = NextResponse.next()
response.headers.set('Content-Security-Policy', generateRuntimeCSP())
setUtmCookie(request, response)
return response
}