Skip to content

Commit 8abe8af

Browse files
committed
update admin routes
1 parent 85284eb commit 8abe8af

File tree

4 files changed

+135
-69
lines changed

4 files changed

+135
-69
lines changed

apps/sim/app/api/v1/admin/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export type {
103103
AdminOrganization,
104104
AdminOrganizationBillingSummary,
105105
AdminOrganizationDetail,
106+
AdminReferralCampaign,
106107
AdminSeatAnalytics,
107108
AdminSingleResponse,
108109
AdminSubscription,
@@ -117,6 +118,7 @@ export type {
117118
AdminWorkspaceMember,
118119
DbMember,
119120
DbOrganization,
121+
DbReferralCampaign,
120122
DbSubscription,
121123
DbUser,
122124
DbUserStats,
@@ -145,6 +147,7 @@ export {
145147
parseWorkflowVariables,
146148
toAdminFolder,
147149
toAdminOrganization,
150+
toAdminReferralCampaign,
148151
toAdminSubscription,
149152
toAdminUser,
150153
toAdminWorkflow,

apps/sim/app/api/v1/admin/referral-campaigns/[id]/route.ts

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,77 +8,81 @@
88
* Update campaign fields. All fields are optional.
99
*
1010
* Body:
11-
* - name?: string — Campaign name (non-empty)
12-
* - bonusCreditAmount?: number Bonus credits in dollars (> 0)
13-
* - isActive?: boolean Enable/disable the campaign
14-
* - code?: string | null — Redeemable code (min 6 chars, auto-uppercased, null to remove)
15-
* - utmSource?: string | null UTM source match (null = wildcard)
16-
* - utmMedium?: string | null UTM medium match (null = wildcard)
17-
* - utmCampaign?: string | null UTM campaign match (null = wildcard)
18-
* - utmContent?: string | null UTM content match (null = wildcard)
11+
* - name: string (non-empty) - Campaign name
12+
* - bonusCreditAmount: number (> 0) - Bonus credits in dollars
13+
* - isActive: boolean - Enable/disable the campaign
14+
* - code: string | null (min 6 chars, auto-uppercased, null to remove) - Redeemable code
15+
* - utmSource: string | null - UTM source match (null = wildcard)
16+
* - utmMedium: string | null - UTM medium match (null = wildcard)
17+
* - utmCampaign: string | null - UTM campaign match (null = wildcard)
18+
* - utmContent: string | null - UTM content match (null = wildcard)
1919
*/
2020

2121
import { db } from '@sim/db'
2222
import { referralCampaigns } from '@sim/db/schema'
2323
import { createLogger } from '@sim/logger'
2424
import { eq } from 'drizzle-orm'
25+
import { getBaseUrl } from '@/lib/core/utils/urls'
2526
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
2627
import {
2728
badRequestResponse,
2829
internalErrorResponse,
2930
notFoundResponse,
3031
singleResponse,
3132
} from '@/app/api/v1/admin/responses'
33+
import { toAdminReferralCampaign } from '@/app/api/v1/admin/types'
3234

33-
const logger = createLogger('AdminReferralCampaign')
35+
const logger = createLogger('AdminReferralCampaignDetailAPI')
3436

3537
interface RouteParams {
3638
id: string
3739
}
3840

39-
export const GET = withAdminAuthParams<RouteParams>(async (_request, context) => {
41+
export const GET = withAdminAuthParams<RouteParams>(async (_, context) => {
4042
try {
41-
const { id } = await context.params
43+
const { id: campaignId } = await context.params
4244

4345
const [campaign] = await db
4446
.select()
4547
.from(referralCampaigns)
46-
.where(eq(referralCampaigns.id, id))
48+
.where(eq(referralCampaigns.id, campaignId))
4749
.limit(1)
4850

4951
if (!campaign) {
5052
return notFoundResponse('Campaign')
5153
}
5254

53-
return singleResponse(campaign)
55+
logger.info(`Admin API: Retrieved referral campaign ${campaignId}`)
56+
57+
return singleResponse(toAdminReferralCampaign(campaign, getBaseUrl()))
5458
} catch (error) {
55-
logger.error('Failed to get referral campaign', { error })
59+
logger.error('Admin API: Failed to get referral campaign', { error })
5660
return internalErrorResponse('Failed to get referral campaign')
5761
}
5862
})
5963

6064
export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) => {
6165
try {
62-
const { id } = await context.params
66+
const { id: campaignId } = await context.params
6367
const body = await request.json()
6468

6569
const [existing] = await db
6670
.select()
6771
.from(referralCampaigns)
68-
.where(eq(referralCampaigns.id, id))
72+
.where(eq(referralCampaigns.id, campaignId))
6973
.limit(1)
7074

7175
if (!existing) {
7276
return notFoundResponse('Campaign')
7377
}
7478

75-
const updates: Record<string, unknown> = { updatedAt: new Date() }
79+
const updateData: Record<string, unknown> = { updatedAt: new Date() }
7680

7781
if (body.name !== undefined) {
78-
if (typeof body.name !== 'string' || !body.name) {
82+
if (typeof body.name !== 'string' || body.name.trim().length === 0) {
7983
return badRequestResponse('name must be a non-empty string')
8084
}
81-
updates.name = body.name
85+
updateData.name = body.name.trim()
8286
}
8387

8488
if (body.bonusCreditAmount !== undefined) {
@@ -89,14 +93,14 @@ export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) =
8993
) {
9094
return badRequestResponse('bonusCreditAmount must be a positive number')
9195
}
92-
updates.bonusCreditAmount = body.bonusCreditAmount.toString()
96+
updateData.bonusCreditAmount = body.bonusCreditAmount.toString()
9397
}
9498

9599
if (body.isActive !== undefined) {
96100
if (typeof body.isActive !== 'boolean') {
97101
return badRequestResponse('isActive must be a boolean')
98102
}
99-
updates.isActive = body.isActive
103+
updateData.isActive = body.isActive
100104
}
101105

102106
if (body.code !== undefined) {
@@ -108,29 +112,31 @@ export const PATCH = withAdminAuthParams<RouteParams>(async (request, context) =
108112
return badRequestResponse('code must be at least 6 characters')
109113
}
110114
}
111-
updates.code = body.code ? body.code.trim().toUpperCase() : null
115+
updateData.code = body.code ? body.code.trim().toUpperCase() : null
112116
}
113117

114118
for (const field of ['utmSource', 'utmMedium', 'utmCampaign', 'utmContent'] as const) {
115119
if (body[field] !== undefined) {
116120
if (body[field] !== null && typeof body[field] !== 'string') {
117121
return badRequestResponse(`${field} must be a string or null`)
118122
}
119-
updates[field] = body[field]
123+
updateData[field] = body[field] || null
120124
}
121125
}
122126

123127
const [updated] = await db
124128
.update(referralCampaigns)
125-
.set(updates)
126-
.where(eq(referralCampaigns.id, id))
129+
.set(updateData)
130+
.where(eq(referralCampaigns.id, campaignId))
127131
.returning()
128132

129-
logger.info('Updated referral campaign', { id, updates })
133+
logger.info(`Admin API: Updated referral campaign ${campaignId}`, {
134+
fields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
135+
})
130136

131-
return singleResponse(updated)
137+
return singleResponse(toAdminReferralCampaign(updated, getBaseUrl()))
132138
} catch (error) {
133-
logger.error('Failed to update referral campaign', { error })
139+
logger.error('Admin API: Failed to update referral campaign', { error })
134140
return internalErrorResponse('Failed to update referral campaign')
135141
}
136142
})

apps/sim/app/api/v1/admin/referral-campaigns/route.ts

Lines changed: 50 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,69 +3,83 @@
33
*
44
* List referral campaigns with optional filtering and pagination.
55
*
6-
* Query params:
7-
* - active?: 'true' | 'false' — Filter by active status
8-
* - limit?: number — Page size (default 50)
9-
* - offset?: number — Offset for pagination
6+
* Query Parameters:
7+
* - active: string (optional) - Filter by active status ('true' or 'false')
8+
* - limit: number (default: 50, max: 250)
9+
* - offset: number (default: 0)
1010
*
1111
* POST /api/v1/admin/referral-campaigns
1212
*
1313
* Create a new referral campaign.
1414
*
1515
* Body:
16-
* - name: string Campaign name (required)
17-
* - bonusCreditAmount: number — Bonus credits in dollars (required, > 0)
18-
* - code?: string | null — Redeemable code (min 6 chars, auto-uppercased)
19-
* - utmSource?: string | null UTM source match (null = wildcard)
20-
* - utmMedium?: string | null UTM medium match (null = wildcard)
21-
* - utmCampaign?: string | null UTM campaign match (null = wildcard)
22-
* - utmContent?: string | null UTM content match (null = wildcard)
16+
* - name: string (required) - Campaign name
17+
* - bonusCreditAmount: number (required, > 0) - Bonus credits in dollars
18+
* - code: string | null (optional, min 6 chars, auto-uppercased) - Redeemable code
19+
* - utmSource: string | null (optional) - UTM source match (null = wildcard)
20+
* - utmMedium: string | null (optional) - UTM medium match (null = wildcard)
21+
* - utmCampaign: string | null (optional) - UTM campaign match (null = wildcard)
22+
* - utmContent: string | null (optional) - UTM content match (null = wildcard)
2323
*/
2424

2525
import { db } from '@sim/db'
2626
import { referralCampaigns } from '@sim/db/schema'
2727
import { createLogger } from '@sim/logger'
28-
import { eq } from 'drizzle-orm'
28+
import { count, eq, type SQL } from 'drizzle-orm'
2929
import { nanoid } from 'nanoid'
30+
import { getBaseUrl } from '@/lib/core/utils/urls'
3031
import { withAdminAuth } from '@/app/api/v1/admin/middleware'
3132
import {
3233
badRequestResponse,
3334
internalErrorResponse,
3435
listResponse,
3536
singleResponse,
3637
} from '@/app/api/v1/admin/responses'
37-
import { createPaginationMeta, parsePaginationParams } from '@/app/api/v1/admin/types'
38+
import {
39+
type AdminReferralCampaign,
40+
createPaginationMeta,
41+
parsePaginationParams,
42+
toAdminReferralCampaign,
43+
} from '@/app/api/v1/admin/types'
3844

39-
const logger = createLogger('AdminReferralCampaigns')
45+
const logger = createLogger('AdminReferralCampaignsAPI')
4046

4147
export const GET = withAdminAuth(async (request) => {
42-
try {
43-
const url = new URL(request.url)
44-
const { limit, offset } = parsePaginationParams(url)
45-
const activeFilter = url.searchParams.get('active')
46-
47-
let query = db.select().from(referralCampaigns).$dynamic()
48+
const url = new URL(request.url)
49+
const { limit, offset } = parsePaginationParams(url)
50+
const activeFilter = url.searchParams.get('active')
4851

52+
try {
53+
const conditions: SQL<unknown>[] = []
4954
if (activeFilter === 'true') {
50-
query = query.where(eq(referralCampaigns.isActive, true))
55+
conditions.push(eq(referralCampaigns.isActive, true))
5156
} else if (activeFilter === 'false') {
52-
query = query.where(eq(referralCampaigns.isActive, false))
57+
conditions.push(eq(referralCampaigns.isActive, false))
5358
}
5459

55-
const rows = await query.limit(limit).offset(offset)
60+
const whereClause = conditions.length > 0 ? conditions[0] : undefined
61+
const baseUrl = getBaseUrl()
5662

57-
let countQuery = db.select().from(referralCampaigns).$dynamic()
58-
if (activeFilter === 'true') {
59-
countQuery = countQuery.where(eq(referralCampaigns.isActive, true))
60-
} else if (activeFilter === 'false') {
61-
countQuery = countQuery.where(eq(referralCampaigns.isActive, false))
62-
}
63-
const allRows = await countQuery
64-
const total = allRows.length
63+
const [countResult, campaigns] = await Promise.all([
64+
db.select({ total: count() }).from(referralCampaigns).where(whereClause),
65+
db
66+
.select()
67+
.from(referralCampaigns)
68+
.where(whereClause)
69+
.orderBy(referralCampaigns.createdAt)
70+
.limit(limit)
71+
.offset(offset),
72+
])
73+
74+
const total = countResult[0].total
75+
const data: AdminReferralCampaign[] = campaigns.map((c) => toAdminReferralCampaign(c, baseUrl))
76+
const pagination = createPaginationMeta(total, limit, offset)
77+
78+
logger.info(`Admin API: Listed ${data.length} referral campaigns (total: ${total})`)
6579

66-
return listResponse(rows, createPaginationMeta(total, limit, offset))
80+
return listResponse(data, pagination)
6781
} catch (error) {
68-
logger.error('Failed to list referral campaigns', { error })
82+
logger.error('Admin API: Failed to list referral campaigns', { error })
6983
return internalErrorResponse('Failed to list referral campaigns')
7084
}
7185
})
@@ -112,20 +126,15 @@ export const POST = withAdminAuth(async (request) => {
112126
})
113127
.returning()
114128

115-
logger.info('Created referral campaign', {
116-
id,
129+
logger.info(`Admin API: Created referral campaign ${id}`, {
117130
name,
118131
code: campaign.code,
119-
utmSource,
120-
utmMedium,
121-
utmCampaign,
122-
utmContent,
123132
bonusCreditAmount,
124133
})
125134

126-
return singleResponse(campaign)
135+
return singleResponse(toAdminReferralCampaign(campaign, getBaseUrl()))
127136
} catch (error) {
128-
logger.error('Failed to create referral campaign', { error })
137+
logger.error('Admin API: Failed to create referral campaign', { error })
129138
return internalErrorResponse('Failed to create referral campaign')
130139
}
131140
})

apps/sim/app/api/v1/admin/types.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import type {
99
member,
1010
organization,
11+
referralCampaigns,
1112
subscription,
1213
user,
1314
userStats,
@@ -31,6 +32,7 @@ export type DbOrganization = InferSelectModel<typeof organization>
3132
export type DbSubscription = InferSelectModel<typeof subscription>
3233
export type DbMember = InferSelectModel<typeof member>
3334
export type DbUserStats = InferSelectModel<typeof userStats>
35+
export type DbReferralCampaign = InferSelectModel<typeof referralCampaigns>
3436

3537
// =============================================================================
3638
// Pagination
@@ -646,3 +648,49 @@ export interface AdminDeployResult {
646648
export interface AdminUndeployResult {
647649
isDeployed: boolean
648650
}
651+
652+
// =============================================================================
653+
// Referral Campaign Types
654+
// =============================================================================
655+
656+
export interface AdminReferralCampaign {
657+
id: string
658+
name: string
659+
code: string | null
660+
utmSource: string | null
661+
utmMedium: string | null
662+
utmCampaign: string | null
663+
utmContent: string | null
664+
bonusCreditAmount: string
665+
isActive: boolean
666+
signupUrl: string | null
667+
createdAt: string
668+
updatedAt: string
669+
}
670+
671+
export function toAdminReferralCampaign(
672+
dbCampaign: DbReferralCampaign,
673+
baseUrl: string
674+
): AdminReferralCampaign {
675+
const utmParams = new URLSearchParams()
676+
if (dbCampaign.utmSource) utmParams.set('utm_source', dbCampaign.utmSource)
677+
if (dbCampaign.utmMedium) utmParams.set('utm_medium', dbCampaign.utmMedium)
678+
if (dbCampaign.utmCampaign) utmParams.set('utm_campaign', dbCampaign.utmCampaign)
679+
if (dbCampaign.utmContent) utmParams.set('utm_content', dbCampaign.utmContent)
680+
const query = utmParams.toString()
681+
682+
return {
683+
id: dbCampaign.id,
684+
name: dbCampaign.name,
685+
code: dbCampaign.code,
686+
utmSource: dbCampaign.utmSource,
687+
utmMedium: dbCampaign.utmMedium,
688+
utmCampaign: dbCampaign.utmCampaign,
689+
utmContent: dbCampaign.utmContent,
690+
bonusCreditAmount: dbCampaign.bonusCreditAmount,
691+
isActive: dbCampaign.isActive,
692+
signupUrl: query ? `${baseUrl}/signup?${query}` : null,
693+
createdAt: dbCampaign.createdAt.toISOString(),
694+
updatedAt: dbCampaign.updatedAt.toISOString(),
695+
}
696+
}

0 commit comments

Comments
 (0)