mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-12 07:24:55 -05:00
more
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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' />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user