Skip to content

Commit a8f87f7

Browse files
authored
feat(creators): add verification for creators (#2135)
1 parent 9330940 commit a8f87f7

File tree

16 files changed

+7944
-21
lines changed

16 files changed

+7944
-21
lines changed

apps/sim/app/api/creator-profiles/[id]/route.ts renamed to apps/sim/app/api/creators/[id]/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ async function hasPermission(userId: string, profile: any): Promise<boolean> {
4545
return false
4646
}
4747

48-
// GET /api/creator-profiles/[id] - Get a specific creator profile
48+
// GET /api/creators/[id] - Get a specific creator profile
4949
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
5050
const requestId = generateRequestId()
5151
const { id } = await params
@@ -70,7 +70,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
7070
}
7171
}
7272

73-
// PUT /api/creator-profiles/[id] - Update a creator profile
73+
// PUT /api/creators/[id] - Update a creator profile
7474
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
7575
const requestId = generateRequestId()
7676
const { id } = await params
@@ -135,7 +135,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
135135
}
136136
}
137137

138-
// DELETE /api/creator-profiles/[id] - Delete a creator profile
138+
// DELETE /api/creators/[id] - Delete a creator profile
139139
export async function DELETE(
140140
request: NextRequest,
141141
{ params }: { params: Promise<{ id: string }> }
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { db } from '@sim/db'
2+
import { templateCreators, user } from '@sim/db/schema'
3+
import { eq } from 'drizzle-orm'
4+
import { type NextRequest, NextResponse } from 'next/server'
5+
import { getSession } from '@/lib/auth'
6+
import { createLogger } from '@/lib/logs/console/logger'
7+
import { generateRequestId } from '@/lib/utils'
8+
9+
const logger = createLogger('CreatorVerificationAPI')
10+
11+
export const revalidate = 0
12+
13+
// POST /api/creators/[id]/verify - Verify a creator (super users only)
14+
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
15+
const requestId = generateRequestId()
16+
const { id } = await params
17+
18+
try {
19+
const session = await getSession()
20+
if (!session?.user?.id) {
21+
logger.warn(`[${requestId}] Unauthorized verification attempt for creator: ${id}`)
22+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
23+
}
24+
25+
// Check if user is a super user
26+
const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
27+
28+
if (!currentUser[0]?.isSuperUser) {
29+
logger.warn(`[${requestId}] Non-super user attempted to verify creator: ${id}`)
30+
return NextResponse.json({ error: 'Only super users can verify creators' }, { status: 403 })
31+
}
32+
33+
// Check if creator exists
34+
const existingCreator = await db
35+
.select()
36+
.from(templateCreators)
37+
.where(eq(templateCreators.id, id))
38+
.limit(1)
39+
40+
if (existingCreator.length === 0) {
41+
logger.warn(`[${requestId}] Creator not found for verification: ${id}`)
42+
return NextResponse.json({ error: 'Creator not found' }, { status: 404 })
43+
}
44+
45+
// Update creator verified status to true
46+
await db
47+
.update(templateCreators)
48+
.set({ verified: true, updatedAt: new Date() })
49+
.where(eq(templateCreators.id, id))
50+
51+
logger.info(`[${requestId}] Creator verified: ${id} by super user: ${session.user.id}`)
52+
53+
return NextResponse.json({
54+
message: 'Creator verified successfully',
55+
creatorId: id,
56+
})
57+
} catch (error) {
58+
logger.error(`[${requestId}] Error verifying creator ${id}`, error)
59+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
60+
}
61+
}
62+
63+
// DELETE /api/creators/[id]/verify - Unverify a creator (super users only)
64+
export async function DELETE(
65+
request: NextRequest,
66+
{ params }: { params: Promise<{ id: string }> }
67+
) {
68+
const requestId = generateRequestId()
69+
const { id } = await params
70+
71+
try {
72+
const session = await getSession()
73+
if (!session?.user?.id) {
74+
logger.warn(`[${requestId}] Unauthorized unverification attempt for creator: ${id}`)
75+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
76+
}
77+
78+
// Check if user is a super user
79+
const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
80+
81+
if (!currentUser[0]?.isSuperUser) {
82+
logger.warn(`[${requestId}] Non-super user attempted to unverify creator: ${id}`)
83+
return NextResponse.json({ error: 'Only super users can unverify creators' }, { status: 403 })
84+
}
85+
86+
// Check if creator exists
87+
const existingCreator = await db
88+
.select()
89+
.from(templateCreators)
90+
.where(eq(templateCreators.id, id))
91+
.limit(1)
92+
93+
if (existingCreator.length === 0) {
94+
logger.warn(`[${requestId}] Creator not found for unverification: ${id}`)
95+
return NextResponse.json({ error: 'Creator not found' }, { status: 404 })
96+
}
97+
98+
// Update creator verified status to false
99+
await db
100+
.update(templateCreators)
101+
.set({ verified: false, updatedAt: new Date() })
102+
.where(eq(templateCreators.id, id))
103+
104+
logger.info(`[${requestId}] Creator unverified: ${id} by super user: ${session.user.id}`)
105+
106+
return NextResponse.json({
107+
message: 'Creator unverified successfully',
108+
creatorId: id,
109+
})
110+
} catch (error) {
111+
logger.error(`[${requestId}] Error unverifying creator ${id}`, error)
112+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
113+
}
114+
}

apps/sim/app/api/creator-profiles/route.ts renamed to apps/sim/app/api/creators/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const CreateCreatorProfileSchema = z.object({
2727
details: CreatorProfileDetailsSchema.optional(),
2828
})
2929

30-
// GET /api/creator-profiles - Get creator profiles for current user
30+
// GET /api/creators - Get creator profiles for current user
3131
export async function GET(request: NextRequest) {
3232
const requestId = generateRequestId()
3333
const { searchParams } = new URL(request.url)
@@ -81,7 +81,7 @@ export async function GET(request: NextRequest) {
8181
}
8282
}
8383

84-
// POST /api/creator-profiles - Create a new creator profile
84+
// POST /api/creators - Create a new creator profile
8585
export async function POST(request: NextRequest) {
8686
const requestId = generateRequestId()
8787

apps/sim/app/templates/[id]/template.tsx

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
DropdownMenuItem,
3030
DropdownMenuTrigger,
3131
} from '@/components/ui/dropdown-menu'
32+
import { VerifiedBadge } from '@/components/ui/verified-badge'
3233
import { useSession } from '@/lib/auth-client'
3334
import { createLogger } from '@/lib/logs/console/logger'
3435
import { getBaseUrl } from '@/lib/urls/utils'
@@ -64,6 +65,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
6465
const [isEditing, setIsEditing] = useState(false)
6566
const [isApproving, setIsApproving] = useState(false)
6667
const [isRejecting, setIsRejecting] = useState(false)
68+
const [isVerifying, setIsVerifying] = useState(false)
6769
const [hasWorkspaceAccess, setHasWorkspaceAccess] = useState<boolean | null>(null)
6870
const [workspaces, setWorkspaces] = useState<
6971
Array<{ id: string; name: string; permissions: string }>
@@ -462,6 +464,32 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
462464
}
463465
}
464466

467+
const handleToggleVerification = async () => {
468+
if (isVerifying || !template?.creator?.id) return
469+
470+
setIsVerifying(true)
471+
try {
472+
const endpoint = `/api/creators/${template.creator.id}/verify`
473+
const method = template.creator.verified ? 'DELETE' : 'POST'
474+
475+
const response = await fetch(endpoint, { method })
476+
477+
if (response.ok) {
478+
// Refresh page to show updated verification status
479+
window.location.reload()
480+
} else {
481+
const error = await response.json()
482+
logger.error('Error toggling verification:', error)
483+
alert(`Failed to ${template.creator.verified ? 'unverify' : 'verify'} creator`)
484+
}
485+
} catch (error) {
486+
logger.error('Error toggling verification:', error)
487+
alert('An error occurred while toggling verification')
488+
} finally {
489+
setIsVerifying(false)
490+
}
491+
}
492+
465493
/**
466494
* Shares the template to X (Twitter)
467495
*/
@@ -718,9 +746,12 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
718746
</div>
719747
)}
720748
{/* Creator name */}
721-
<span className='font-medium text-[#8B8B8B] text-[14px]'>
722-
{template.creator?.name || 'Unknown'}
723-
</span>
749+
<div className='flex items-center gap-[4px]'>
750+
<span className='font-medium text-[#8B8B8B] text-[14px]'>
751+
{template.creator?.name || 'Unknown'}
752+
</span>
753+
{template.creator?.verified && <VerifiedBadge size='md' />}
754+
</div>
724755
</div>
725756

726757
{/* Credentials needed */}
@@ -849,9 +880,25 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
849880
template.creator.details?.websiteUrl ||
850881
template.creator.details?.contactEmail) && (
851882
<div className='mt-8'>
852-
<h3 className='mb-4 font-sans font-semibold text-base text-foreground'>
853-
About the Creator
854-
</h3>
883+
<div className='mb-4 flex items-center justify-between'>
884+
<h3 className='font-sans font-semibold text-base text-foreground'>
885+
About the Creator
886+
</h3>
887+
{isSuperUser && template.creator && (
888+
<Button
889+
variant={template.creator.verified ? 'active' : 'default'}
890+
onClick={handleToggleVerification}
891+
disabled={isVerifying}
892+
className='h-[28px] rounded-[6px] text-[12px]'
893+
>
894+
{isVerifying
895+
? 'Updating...'
896+
: template.creator.verified
897+
? 'Unverify Creator'
898+
: 'Verify Creator'}
899+
</Button>
900+
)}
901+
</div>
855902
<div className='flex items-start gap-4'>
856903
{/* Creator profile image */}
857904
{template.creator.profileImageUrl ? (
@@ -871,9 +918,12 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
871918
{/* Creator details */}
872919
<div className='flex-1'>
873920
<div className='mb-[5px] flex items-center gap-3'>
874-
<h4 className='font-sans font-semibold text-base text-foreground'>
875-
{template.creator.name}
876-
</h4>
921+
<div className='flex items-center gap-[6px]'>
922+
<h4 className='font-sans font-semibold text-base text-foreground'>
923+
{template.creator.name}
924+
</h4>
925+
{template.creator.verified && <VerifiedBadge size='md' />}
926+
</div>
877927

878928
{/* Social links */}
879929
<div className='flex items-center gap-[12px]'>

apps/sim/app/templates/components/template-card.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { Star, User } from 'lucide-react'
33
import { useParams, useRouter } from 'next/navigation'
4+
import { VerifiedBadge } from '@/components/ui/verified-badge'
45
import { createLogger } from '@/lib/logs/console/logger'
56
import { cn } from '@/lib/utils'
67
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
@@ -21,6 +22,7 @@ interface TemplateCardProps {
2122
className?: string
2223
state?: WorkflowState
2324
isStarred?: boolean
25+
isVerified?: boolean
2426
}
2527

2628
export function TemplateCardSkeleton({ className }: { className?: string }) {
@@ -125,6 +127,7 @@ function TemplateCardInner({
125127
className,
126128
state,
127129
isStarred = false,
130+
isVerified = false,
128131
}: TemplateCardProps) {
129132
const router = useRouter()
130133
const params = useParams()
@@ -276,7 +279,10 @@ function TemplateCardInner({
276279
<User className='h-[18px] w-[18px] text-[#888888]' />
277280
</div>
278281
)}
279-
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
282+
<div className='flex items-center gap-[4px]'>
283+
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
284+
{isVerified && <VerifiedBadge size='sm' />}
285+
</div>
280286
</div>
281287

282288
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[#888888] text-[12px]'>

apps/sim/app/templates/templates.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface Template {
3030
details?: CreatorProfileDetails | null
3131
referenceType: 'user' | 'organization'
3232
referenceId: string
33+
verified?: boolean
3334
} | null
3435
views: number
3536
stars: number
@@ -203,6 +204,7 @@ export default function Templates({
203204
stars={template.stars}
204205
state={template.state}
205206
isStarred={template.isStarred}
207+
isVerified={template.creator?.verified || false}
206208
/>
207209
))
208210
)}

apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
22
import { Star, User } from 'lucide-react'
33
import { useParams, useRouter } from 'next/navigation'
4+
import { VerifiedBadge } from '@/components/ui/verified-badge'
45
import { createLogger } from '@/lib/logs/console/logger'
56
import { cn } from '@/lib/utils'
67
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
@@ -21,6 +22,7 @@ interface TemplateCardProps {
2122
className?: string
2223
state?: WorkflowState
2324
isStarred?: boolean
25+
isVerified?: boolean
2426
}
2527

2628
export function TemplateCardSkeleton({ className }: { className?: string }) {
@@ -126,6 +128,7 @@ function TemplateCardInner({
126128
className,
127129
state,
128130
isStarred = false,
131+
isVerified = false,
129132
}: TemplateCardProps) {
130133
const router = useRouter()
131134
const params = useParams()
@@ -277,7 +280,10 @@ function TemplateCardInner({
277280
<User className='h-[18px] w-[18px] text-[#888888]' />
278281
</div>
279282
)}
280-
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
283+
<div className='flex items-center gap-[4px]'>
284+
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
285+
{isVerified && <VerifiedBadge size='sm' />}
286+
</div>
281287
</div>
282288

283289
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[#888888] text-[12px]'>

apps/sim/app/workspace/[workspaceId]/templates/templates.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export interface Template {
3434
details?: CreatorProfileDetails | null
3535
referenceType: 'user' | 'organization'
3636
referenceId: string
37+
verified?: boolean
3738
} | null
3839
views: number
3940
stars: number
@@ -223,6 +224,7 @@ export default function Templates({
223224
stars={template.stars}
224225
state={template.state}
225226
isStarred={template.isStarred}
227+
isVerified={template.creator?.verified || false}
226228
/>
227229
)
228230
})

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template-deploy.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function TemplateDeploy({ workflowId, onDeploymentComplete }: TemplateDep
8787

8888
setLoadingCreators(true)
8989
try {
90-
const response = await fetch('/api/creator-profiles')
90+
const response = await fetch('/api/creators')
9191
if (response.ok) {
9292
const data = await response.json()
9393
const profiles = (data.profiles || []).map((profile: any) => ({

0 commit comments

Comments
 (0)