update admin routes

This commit is contained in:
Waleed Latif
2026-02-11 19:57:46 -08:00
parent 85284eb7c4
commit 8abe8af289
4 changed files with 135 additions and 69 deletions

View File

@@ -103,6 +103,7 @@ export type {
AdminOrganization,
AdminOrganizationBillingSummary,
AdminOrganizationDetail,
AdminReferralCampaign,
AdminSeatAnalytics,
AdminSingleResponse,
AdminSubscription,
@@ -117,6 +118,7 @@ export type {
AdminWorkspaceMember,
DbMember,
DbOrganization,
DbReferralCampaign,
DbSubscription,
DbUser,
DbUserStats,
@@ -145,6 +147,7 @@ export {
parseWorkflowVariables,
toAdminFolder,
toAdminOrganization,
toAdminReferralCampaign,
toAdminSubscription,
toAdminUser,
toAdminWorkflow,

View File

@@ -8,20 +8,21 @@
* Update campaign fields. All fields are optional.
*
* Body:
* - name?: string Campaign name (non-empty)
* - bonusCreditAmount?: number Bonus credits in dollars (> 0)
* - isActive?: boolean Enable/disable the campaign
* - code?: string | null — Redeemable code (min 6 chars, auto-uppercased, null to remove)
* - utmSource?: string | null UTM source match (null = wildcard)
* - utmMedium?: string | null UTM medium match (null = wildcard)
* - utmCampaign?: string | null UTM campaign match (null = wildcard)
* - utmContent?: string | null UTM content match (null = wildcard)
* - name: string (non-empty) - Campaign name
* - bonusCreditAmount: number (> 0) - Bonus credits in dollars
* - isActive: boolean - Enable/disable the campaign
* - code: string | null (min 6 chars, auto-uppercased, null to remove) - Redeemable code
* - utmSource: string | null - UTM source match (null = wildcard)
* - utmMedium: string | null - UTM medium match (null = wildcard)
* - utmCampaign: string | null - UTM campaign match (null = wildcard)
* - utmContent: string | null - UTM content match (null = wildcard)
*/
import { db } from '@sim/db'
import { referralCampaigns } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
badRequestResponse,
@@ -29,56 +30,59 @@ import {
notFoundResponse,
singleResponse,
} from '@/app/api/v1/admin/responses'
import { toAdminReferralCampaign } from '@/app/api/v1/admin/types'
const logger = createLogger('AdminReferralCampaign')
const logger = createLogger('AdminReferralCampaignDetailAPI')
interface RouteParams {
id: string
}
export const GET = withAdminAuthParams<RouteParams>(async (_request, context) => {
export const GET = withAdminAuthParams<RouteParams>(async (_, context) => {
try {
const { id } = await context.params
const { id: campaignId } = await context.params
const [campaign] = await db
.select()
.from(referralCampaigns)
.where(eq(referralCampaigns.id, id))
.where(eq(referralCampaigns.id, campaignId))
.limit(1)
if (!campaign) {
return notFoundResponse('Campaign')
}
return singleResponse(campaign)
logger.info(`Admin API: Retrieved referral campaign ${campaignId}`)
return singleResponse(toAdminReferralCampaign(campaign, getBaseUrl()))
} catch (error) {
logger.error('Failed to get referral campaign', { error })
logger.error('Admin API: Failed to get referral campaign', { error })
return internalErrorResponse('Failed to get referral campaign')
}
})
export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) => {
try {
const { id } = await context.params
const { id: campaignId } = await context.params
const body = await request.json()
const [existing] = await db
.select()
.from(referralCampaigns)
.where(eq(referralCampaigns.id, id))
.where(eq(referralCampaigns.id, campaignId))
.limit(1)
if (!existing) {
return notFoundResponse('Campaign')
}
const updates: Record<string, unknown> = { updatedAt: new Date() }
const updateData: Record<string, unknown> = { updatedAt: new Date() }
if (body.name !== undefined) {
if (typeof body.name !== 'string' || !body.name) {
if (typeof body.name !== 'string' || body.name.trim().length === 0) {
return badRequestResponse('name must be a non-empty string')
}
updates.name = body.name
updateData.name = body.name.trim()
}
if (body.bonusCreditAmount !== undefined) {
@@ -89,14 +93,14 @@ export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) =
) {
return badRequestResponse('bonusCreditAmount must be a positive number')
}
updates.bonusCreditAmount = body.bonusCreditAmount.toString()
updateData.bonusCreditAmount = body.bonusCreditAmount.toString()
}
if (body.isActive !== undefined) {
if (typeof body.isActive !== 'boolean') {
return badRequestResponse('isActive must be a boolean')
}
updates.isActive = body.isActive
updateData.isActive = body.isActive
}
if (body.code !== undefined) {
@@ -108,7 +112,7 @@ export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) =
return badRequestResponse('code must be at least 6 characters')
}
}
updates.code = body.code ? body.code.trim().toUpperCase() : null
updateData.code = body.code ? body.code.trim().toUpperCase() : null
}
for (const field of ['utmSource', 'utmMedium', 'utmCampaign', 'utmContent'] as const) {
@@ -116,21 +120,23 @@ export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) =
if (body[field] !== null && typeof body[field] !== 'string') {
return badRequestResponse(`${field} must be a string or null`)
}
updates[field] = body[field]
updateData[field] = body[field] || null
}
}
const [updated] = await db
.update(referralCampaigns)
.set(updates)
.where(eq(referralCampaigns.id, id))
.set(updateData)
.where(eq(referralCampaigns.id, campaignId))
.returning()
logger.info('Updated referral campaign', { id, updates })
logger.info(`Admin API: Updated referral campaign ${campaignId}`, {
fields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
})
return singleResponse(updated)
return singleResponse(toAdminReferralCampaign(updated, getBaseUrl()))
} catch (error) {
logger.error('Failed to update referral campaign', { error })
logger.error('Admin API: Failed to update referral campaign', { error })
return internalErrorResponse('Failed to update referral campaign')
}
})

View File

@@ -3,30 +3,31 @@
*
* List referral campaigns with optional filtering and pagination.
*
* Query params:
* - active?: 'true' | 'false' — Filter by active status
* - limit?: number — Page size (default 50)
* - offset?: number — Offset for pagination
* Query Parameters:
* - active: string (optional) - Filter by active status ('true' or 'false')
* - limit: number (default: 50, max: 250)
* - offset: number (default: 0)
*
* POST /api/v1/admin/referral-campaigns
*
* Create a new referral campaign.
*
* Body:
* - name: string Campaign name (required)
* - bonusCreditAmount: number Bonus credits in dollars (required, > 0)
* - code?: string | null — Redeemable code (min 6 chars, auto-uppercased)
* - utmSource?: string | null UTM source match (null = wildcard)
* - utmMedium?: string | null UTM medium match (null = wildcard)
* - utmCampaign?: string | null UTM campaign match (null = wildcard)
* - utmContent?: string | null UTM content match (null = wildcard)
* - name: string (required) - Campaign name
* - bonusCreditAmount: number (required, > 0) - Bonus credits in dollars
* - code: string | null (optional, min 6 chars, auto-uppercased) - Redeemable code
* - utmSource: string | null (optional) - UTM source match (null = wildcard)
* - utmMedium: string | null (optional) - UTM medium match (null = wildcard)
* - utmCampaign: string | null (optional) - UTM campaign match (null = wildcard)
* - utmContent: string | null (optional) - UTM content match (null = wildcard)
*/
import { db } from '@sim/db'
import { referralCampaigns } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { count, eq, type SQL } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { withAdminAuth } from '@/app/api/v1/admin/middleware'
import {
badRequestResponse,
@@ -34,38 +35,51 @@ import {
listResponse,
singleResponse,
} from '@/app/api/v1/admin/responses'
import { createPaginationMeta, parsePaginationParams } from '@/app/api/v1/admin/types'
import {
type AdminReferralCampaign,
createPaginationMeta,
parsePaginationParams,
toAdminReferralCampaign,
} from '@/app/api/v1/admin/types'
const logger = createLogger('AdminReferralCampaigns')
const logger = createLogger('AdminReferralCampaignsAPI')
export const GET = withAdminAuth(async (request) => {
const url = new URL(request.url)
const { limit, offset } = parsePaginationParams(url)
const activeFilter = url.searchParams.get('active')
try {
const url = new URL(request.url)
const { limit, offset } = parsePaginationParams(url)
const activeFilter = url.searchParams.get('active')
let query = db.select().from(referralCampaigns).$dynamic()
const conditions: SQL<unknown>[] = []
if (activeFilter === 'true') {
query = query.where(eq(referralCampaigns.isActive, true))
conditions.push(eq(referralCampaigns.isActive, true))
} else if (activeFilter === 'false') {
query = query.where(eq(referralCampaigns.isActive, false))
conditions.push(eq(referralCampaigns.isActive, false))
}
const rows = await query.limit(limit).offset(offset)
const whereClause = conditions.length > 0 ? conditions[0] : undefined
const baseUrl = getBaseUrl()
let countQuery = db.select().from(referralCampaigns).$dynamic()
if (activeFilter === 'true') {
countQuery = countQuery.where(eq(referralCampaigns.isActive, true))
} else if (activeFilter === 'false') {
countQuery = countQuery.where(eq(referralCampaigns.isActive, false))
}
const allRows = await countQuery
const total = allRows.length
const [countResult, campaigns] = await Promise.all([
db.select({ total: count() }).from(referralCampaigns).where(whereClause),
db
.select()
.from(referralCampaigns)
.where(whereClause)
.orderBy(referralCampaigns.createdAt)
.limit(limit)
.offset(offset),
])
return listResponse(rows, createPaginationMeta(total, limit, offset))
const total = countResult[0].total
const data: AdminReferralCampaign[] = campaigns.map((c) => toAdminReferralCampaign(c, baseUrl))
const pagination = createPaginationMeta(total, limit, offset)
logger.info(`Admin API: Listed ${data.length} referral campaigns (total: ${total})`)
return listResponse(data, pagination)
} catch (error) {
logger.error('Failed to list referral campaigns', { error })
logger.error('Admin API: Failed to list referral campaigns', { error })
return internalErrorResponse('Failed to list referral campaigns')
}
})
@@ -112,20 +126,15 @@ export const POST = withAdminAuth(async (request) => {
})
.returning()
logger.info('Created referral campaign', {
id,
logger.info(`Admin API: Created referral campaign ${id}`, {
name,
code: campaign.code,
utmSource,
utmMedium,
utmCampaign,
utmContent,
bonusCreditAmount,
})
return singleResponse(campaign)
return singleResponse(toAdminReferralCampaign(campaign, getBaseUrl()))
} catch (error) {
logger.error('Failed to create referral campaign', { error })
logger.error('Admin API: Failed to create referral campaign', { error })
return internalErrorResponse('Failed to create referral campaign')
}
})

View File

@@ -8,6 +8,7 @@
import type {
member,
organization,
referralCampaigns,
subscription,
user,
userStats,
@@ -31,6 +32,7 @@ export type DbOrganization = InferSelectModel<typeof organization>
export type DbSubscription = InferSelectModel<typeof subscription>
export type DbMember = InferSelectModel<typeof member>
export type DbUserStats = InferSelectModel<typeof userStats>
export type DbReferralCampaign = InferSelectModel<typeof referralCampaigns>
// =============================================================================
// Pagination
@@ -646,3 +648,49 @@ export interface AdminDeployResult {
export interface AdminUndeployResult {
isDeployed: boolean
}
// =============================================================================
// Referral Campaign Types
// =============================================================================
export interface AdminReferralCampaign {
id: string
name: string
code: string | null
utmSource: string | null
utmMedium: string | null
utmCampaign: string | null
utmContent: string | null
bonusCreditAmount: string
isActive: boolean
signupUrl: string | null
createdAt: string
updatedAt: string
}
export function toAdminReferralCampaign(
dbCampaign: DbReferralCampaign,
baseUrl: string
): AdminReferralCampaign {
const utmParams = new URLSearchParams()
if (dbCampaign.utmSource) utmParams.set('utm_source', dbCampaign.utmSource)
if (dbCampaign.utmMedium) utmParams.set('utm_medium', dbCampaign.utmMedium)
if (dbCampaign.utmCampaign) utmParams.set('utm_campaign', dbCampaign.utmCampaign)
if (dbCampaign.utmContent) utmParams.set('utm_content', dbCampaign.utmContent)
const query = utmParams.toString()
return {
id: dbCampaign.id,
name: dbCampaign.name,
code: dbCampaign.code,
utmSource: dbCampaign.utmSource,
utmMedium: dbCampaign.utmMedium,
utmCampaign: dbCampaign.utmCampaign,
utmContent: dbCampaign.utmContent,
bonusCreditAmount: dbCampaign.bonusCreditAmount,
isActive: dbCampaign.isActive,
signupUrl: query ? `${baseUrl}/signup?${query}` : null,
createdAt: dbCampaign.createdAt.toISOString(),
updatedAt: dbCampaign.updatedAt.toISOString(),
}
}