added zod

This commit is contained in:
Waleed Latif
2026-02-11 12:29:37 -08:00
parent 9228893c19
commit 05d1c92e1a
2 changed files with 25 additions and 11 deletions

View File

@@ -18,6 +18,7 @@ import { eq } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { applyBonusCredits } from '@/lib/billing/credits/bonus'
@@ -26,11 +27,21 @@ const logger = createLogger('AttributionAPI')
const COOKIE_NAME = 'sim_utm'
const CLOCK_DRIFT_TOLERANCE_MS = 60 * 1000
const UtmCookieSchema = z.object({
utm_source: z.string().optional(),
utm_medium: z.string().optional(),
utm_campaign: z.string().optional(),
utm_content: z.string().optional(),
referrer_url: z.string().optional(),
landing_page: z.string().optional(),
created_at: z.string().min(1),
})
/**
* Finds the most specific active campaign matching the given UTM params.
* Null fields on a campaign act as wildcards. Ties broken by newest campaign.
*/
async function findMatchingCampaign(utmData: Record<string, string>) {
async function findMatchingCampaign(utmData: z.infer<typeof UtmCookieSchema>) {
const campaigns = await db
.select()
.from(referralCampaigns)
@@ -89,16 +100,15 @@ export async function POST() {
return NextResponse.json({ attributed: false, reason: 'no_utm_cookie' })
}
let utmData: Record<string, string>
let utmData: z.infer<typeof UtmCookieSchema>
try {
// Decode first, falling back to raw value if UTM params contain bare %
let decoded: string
try {
decoded = decodeURIComponent(utmCookie.value)
} catch {
decoded = utmCookie.value
}
utmData = JSON.parse(decoded)
utmData = UtmCookieSchema.parse(JSON.parse(decoded))
} catch {
logger.warn('Failed to parse UTM cookie', { userId: session.user.id })
cookieStore.delete(COOKIE_NAME)
@@ -106,8 +116,8 @@ export async function POST() {
}
const cookieCreatedAt = Number(utmData.created_at)
if (!cookieCreatedAt || !Number.isFinite(cookieCreatedAt)) {
logger.warn('UTM cookie missing created_at timestamp', { userId: session.user.id })
if (!Number.isFinite(cookieCreatedAt)) {
logger.warn('UTM cookie has invalid created_at timestamp', { userId: session.user.id })
cookieStore.delete(COOKIE_NAME)
return NextResponse.json({ attributed: false, reason: 'invalid_cookie' })
}

View File

@@ -20,12 +20,17 @@ import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { applyBonusCredits } from '@/lib/billing/credits/bonus'
const logger = createLogger('ReferralCodeRedemption')
const RedeemCodeSchema = z.object({
code: z.string().min(1, 'Code is required'),
})
export async function POST(request: Request) {
try {
const session = await getSession()
@@ -34,11 +39,7 @@ export async function POST(request: Request) {
}
const body = await request.json()
const { code } = body
if (!code || typeof code !== 'string') {
return NextResponse.json({ error: 'Code is required' }, { status: 400 })
}
const { code } = RedeemCodeSchema.parse(body)
const subscription = await getHighestPrioritySubscription(session.user.id)
@@ -160,6 +161,9 @@ export async function POST(request: Request) {
bonusAmount,
})
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: error.errors[0].message }, { status: 400 })
}
logger.error('Referral code redemption error', { error })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}