feat(creators): add verification for creators (#2135)

This commit is contained in:
Waleed
2025-11-29 20:58:18 -08:00
committed by GitHub
parent 9330940658
commit a8f87f7e3a
16 changed files with 7944 additions and 21 deletions

View File

@@ -45,7 +45,7 @@ async function hasPermission(userId: string, profile: any): Promise<boolean> {
return false
}
// GET /api/creator-profiles/[id] - Get a specific creator profile
// GET /api/creators/[id] - Get a specific creator profile
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params
@@ -70,7 +70,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
}
}
// PUT /api/creator-profiles/[id] - Update a creator profile
// PUT /api/creators/[id] - Update a creator profile
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params
@@ -135,7 +135,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
}
}
// DELETE /api/creator-profiles/[id] - Delete a creator profile
// DELETE /api/creators/[id] - Delete a creator profile
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }

View File

@@ -0,0 +1,114 @@
import { db } from '@sim/db'
import { templateCreators, user } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
const logger = createLogger('CreatorVerificationAPI')
export const revalidate = 0
// POST /api/creators/[id]/verify - Verify a creator (super users only)
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = generateRequestId()
const { id } = await params
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized verification attempt for creator: ${id}`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Check if user is a super user
const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
if (!currentUser[0]?.isSuperUser) {
logger.warn(`[${requestId}] Non-super user attempted to verify creator: ${id}`)
return NextResponse.json({ error: 'Only super users can verify creators' }, { status: 403 })
}
// Check if creator exists
const existingCreator = await db
.select()
.from(templateCreators)
.where(eq(templateCreators.id, id))
.limit(1)
if (existingCreator.length === 0) {
logger.warn(`[${requestId}] Creator not found for verification: ${id}`)
return NextResponse.json({ error: 'Creator not found' }, { status: 404 })
}
// Update creator verified status to true
await db
.update(templateCreators)
.set({ verified: true, updatedAt: new Date() })
.where(eq(templateCreators.id, id))
logger.info(`[${requestId}] Creator verified: ${id} by super user: ${session.user.id}`)
return NextResponse.json({
message: 'Creator verified successfully',
creatorId: id,
})
} catch (error) {
logger.error(`[${requestId}] Error verifying creator ${id}`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
// DELETE /api/creators/[id]/verify - Unverify a creator (super users only)
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = generateRequestId()
const { id } = await params
try {
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized unverification attempt for creator: ${id}`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Check if user is a super user
const currentUser = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
if (!currentUser[0]?.isSuperUser) {
logger.warn(`[${requestId}] Non-super user attempted to unverify creator: ${id}`)
return NextResponse.json({ error: 'Only super users can unverify creators' }, { status: 403 })
}
// Check if creator exists
const existingCreator = await db
.select()
.from(templateCreators)
.where(eq(templateCreators.id, id))
.limit(1)
if (existingCreator.length === 0) {
logger.warn(`[${requestId}] Creator not found for unverification: ${id}`)
return NextResponse.json({ error: 'Creator not found' }, { status: 404 })
}
// Update creator verified status to false
await db
.update(templateCreators)
.set({ verified: false, updatedAt: new Date() })
.where(eq(templateCreators.id, id))
logger.info(`[${requestId}] Creator unverified: ${id} by super user: ${session.user.id}`)
return NextResponse.json({
message: 'Creator unverified successfully',
creatorId: id,
})
} catch (error) {
logger.error(`[${requestId}] Error unverifying creator ${id}`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

View File

@@ -27,7 +27,7 @@ const CreateCreatorProfileSchema = z.object({
details: CreatorProfileDetailsSchema.optional(),
})
// GET /api/creator-profiles - Get creator profiles for current user
// GET /api/creators - Get creator profiles for current user
export async function GET(request: NextRequest) {
const requestId = generateRequestId()
const { searchParams } = new URL(request.url)
@@ -81,7 +81,7 @@ export async function GET(request: NextRequest) {
}
}
// POST /api/creator-profiles - Create a new creator profile
// POST /api/creators - Create a new creator profile
export async function POST(request: NextRequest) {
const requestId = generateRequestId()

View File

@@ -29,6 +29,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { VerifiedBadge } from '@/components/ui/verified-badge'
import { useSession } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console/logger'
import { getBaseUrl } from '@/lib/urls/utils'
@@ -64,6 +65,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
const [isEditing, setIsEditing] = useState(false)
const [isApproving, setIsApproving] = useState(false)
const [isRejecting, setIsRejecting] = useState(false)
const [isVerifying, setIsVerifying] = useState(false)
const [hasWorkspaceAccess, setHasWorkspaceAccess] = useState<boolean | null>(null)
const [workspaces, setWorkspaces] = useState<
Array<{ id: string; name: string; permissions: string }>
@@ -462,6 +464,32 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
}
}
const handleToggleVerification = async () => {
if (isVerifying || !template?.creator?.id) return
setIsVerifying(true)
try {
const endpoint = `/api/creators/${template.creator.id}/verify`
const method = template.creator.verified ? 'DELETE' : 'POST'
const response = await fetch(endpoint, { method })
if (response.ok) {
// Refresh page to show updated verification status
window.location.reload()
} else {
const error = await response.json()
logger.error('Error toggling verification:', error)
alert(`Failed to ${template.creator.verified ? 'unverify' : 'verify'} creator`)
}
} catch (error) {
logger.error('Error toggling verification:', error)
alert('An error occurred while toggling verification')
} finally {
setIsVerifying(false)
}
}
/**
* Shares the template to X (Twitter)
*/
@@ -718,9 +746,12 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
</div>
)}
{/* Creator name */}
<span className='font-medium text-[#8B8B8B] text-[14px]'>
{template.creator?.name || 'Unknown'}
</span>
<div className='flex items-center gap-[4px]'>
<span className='font-medium text-[#8B8B8B] text-[14px]'>
{template.creator?.name || 'Unknown'}
</span>
{template.creator?.verified && <VerifiedBadge size='md' />}
</div>
</div>
{/* Credentials needed */}
@@ -849,9 +880,25 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
template.creator.details?.websiteUrl ||
template.creator.details?.contactEmail) && (
<div className='mt-8'>
<h3 className='mb-4 font-sans font-semibold text-base text-foreground'>
About the Creator
</h3>
<div className='mb-4 flex items-center justify-between'>
<h3 className='font-sans font-semibold text-base text-foreground'>
About the Creator
</h3>
{isSuperUser && template.creator && (
<Button
variant={template.creator.verified ? 'active' : 'default'}
onClick={handleToggleVerification}
disabled={isVerifying}
className='h-[28px] rounded-[6px] text-[12px]'
>
{isVerifying
? 'Updating...'
: template.creator.verified
? 'Unverify Creator'
: 'Verify Creator'}
</Button>
)}
</div>
<div className='flex items-start gap-4'>
{/* Creator profile image */}
{template.creator.profileImageUrl ? (
@@ -871,9 +918,12 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
{/* Creator details */}
<div className='flex-1'>
<div className='mb-[5px] flex items-center gap-3'>
<h4 className='font-sans font-semibold text-base text-foreground'>
{template.creator.name}
</h4>
<div className='flex items-center gap-[6px]'>
<h4 className='font-sans font-semibold text-base text-foreground'>
{template.creator.name}
</h4>
{template.creator.verified && <VerifiedBadge size='md' />}
</div>
{/* Social links */}
<div className='flex items-center gap-[12px]'>

View File

@@ -1,6 +1,7 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Star, User } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { VerifiedBadge } from '@/components/ui/verified-badge'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
@@ -21,6 +22,7 @@ interface TemplateCardProps {
className?: string
state?: WorkflowState
isStarred?: boolean
isVerified?: boolean
}
export function TemplateCardSkeleton({ className }: { className?: string }) {
@@ -125,6 +127,7 @@ function TemplateCardInner({
className,
state,
isStarred = false,
isVerified = false,
}: TemplateCardProps) {
const router = useRouter()
const params = useParams()
@@ -276,7 +279,10 @@ function TemplateCardInner({
<User className='h-[18px] w-[18px] text-[#888888]' />
</div>
)}
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
<div className='flex items-center gap-[4px]'>
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
{isVerified && <VerifiedBadge size='sm' />}
</div>
</div>
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[#888888] text-[12px]'>

View File

@@ -30,6 +30,7 @@ export interface Template {
details?: CreatorProfileDetails | null
referenceType: 'user' | 'organization'
referenceId: string
verified?: boolean
} | null
views: number
stars: number
@@ -203,6 +204,7 @@ export default function Templates({
stars={template.stars}
state={template.state}
isStarred={template.isStarred}
isVerified={template.creator?.verified || false}
/>
))
)}

View File

@@ -1,6 +1,7 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Star, User } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { VerifiedBadge } from '@/components/ui/verified-badge'
import { createLogger } from '@/lib/logs/console/logger'
import { cn } from '@/lib/utils'
import { WorkflowPreview } from '@/app/workspace/[workspaceId]/w/components/workflow-preview/workflow-preview'
@@ -21,6 +22,7 @@ interface TemplateCardProps {
className?: string
state?: WorkflowState
isStarred?: boolean
isVerified?: boolean
}
export function TemplateCardSkeleton({ className }: { className?: string }) {
@@ -126,6 +128,7 @@ function TemplateCardInner({
className,
state,
isStarred = false,
isVerified = false,
}: TemplateCardProps) {
const router = useRouter()
const params = useParams()
@@ -277,7 +280,10 @@ function TemplateCardInner({
<User className='h-[18px] w-[18px] text-[#888888]' />
</div>
)}
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
<div className='flex items-center gap-[4px]'>
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
{isVerified && <VerifiedBadge size='sm' />}
</div>
</div>
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[#888888] text-[12px]'>

View File

@@ -34,6 +34,7 @@ export interface Template {
details?: CreatorProfileDetails | null
referenceType: 'user' | 'organization'
referenceId: string
verified?: boolean
} | null
views: number
stars: number
@@ -223,6 +224,7 @@ export default function Templates({
stars={template.stars}
state={template.state}
isStarred={template.isStarred}
isVerified={template.creator?.verified || false}
/>
)
})

View File

@@ -87,7 +87,7 @@ export function TemplateDeploy({ workflowId, onDeploymentComplete }: TemplateDep
setLoadingCreators(true)
try {
const response = await fetch('/api/creator-profiles')
const response = await fetch('/api/creators')
if (response.ok) {
const data = await response.json()
const profiles = (data.profiles || []).map((profile: any) => ({

View File

@@ -0,0 +1,33 @@
import { cn } from '@/lib/utils'
interface VerifiedBadgeProps {
className?: string
size?: 'sm' | 'md' | 'lg'
}
export function VerifiedBadge({ className, size = 'md' }: VerifiedBadgeProps) {
const sizeMap = {
sm: 12,
md: 14,
lg: 16,
}
const dimension = sizeMap[size]
return (
<div className={cn('inline-flex flex-shrink-0', className)} title='Verified Creator'>
<svg
width={dimension}
height={dimension}
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M16 8.375C16 8.93437 15.8656 9.45312 15.5969 9.92813C15.3281 10.4031 14.9688 10.775 14.5156 11.0344C14.5281 11.1188 14.5344 11.25 14.5344 11.4281C14.5344 12.275 14.25 12.9937 13.6875 13.5875C13.1219 14.1844 12.4406 14.4812 11.6438 14.4812C11.2875 14.4812 10.9469 14.4156 10.625 14.2844C10.375 14.7969 10.0156 15.2094 9.54375 15.525C9.075 15.8438 8.55937 16 8 16C7.42812 16 6.90938 15.8469 6.44688 15.5344C5.98125 15.225 5.625 14.8094 5.375 14.2844C5.05312 14.4156 4.71562 14.4812 4.35625 14.4812C3.55937 14.4812 2.875 14.1844 2.30312 13.5875C1.73125 12.9937 1.44687 12.2719 1.44687 11.4281C1.44687 11.3344 1.45938 11.2031 1.48125 11.0344C1.02813 10.7719 0.66875 10.4031 0.4 9.92813C0.134375 9.45312 0 8.93437 0 8.375C0 7.78125 0.15 7.23438 0.446875 6.74062C0.74375 6.24687 1.14375 5.88125 1.64375 5.64375C1.5125 5.2875 1.44687 4.92812 1.44687 4.57188C1.44687 3.72813 1.73125 3.00625 2.30312 2.4125C2.875 1.81875 3.55937 1.51875 4.35625 1.51875C4.7125 1.51875 5.05312 1.58438 5.375 1.71563C5.625 1.20312 5.98438 0.790625 6.45625 0.475C6.925 0.159375 7.44063 0 8 0C8.55937 0 9.075 0.159375 9.54375 0.471875C10.0125 0.7875 10.375 1.2 10.625 1.7125C10.9469 1.58125 11.2844 1.51562 11.6438 1.51562C12.4406 1.51562 13.1219 1.8125 13.6875 2.40937C14.2531 3.00625 14.5344 3.725 14.5344 4.56875C14.5344 4.9625 14.475 5.31875 14.3562 5.64062C14.8562 5.87813 15.2563 6.24375 15.5531 6.7375C15.85 7.23438 16 7.78125 16 8.375ZM7.65938 10.7844L10.9625 5.8375C11.0469 5.70625 11.0719 5.5625 11.0437 5.40938C11.0125 5.25625 10.9344 5.13438 10.8031 5.05312C10.6719 4.96875 10.5281 4.94063 10.375 4.9625C10.2188 4.9875 10.0938 5.0625 10 5.19375L7.09062 9.56875L5.75 8.23125C5.63125 8.1125 5.49375 8.05625 5.34062 8.0625C5.18437 8.06875 5.05 8.125 4.93125 8.23125C4.825 8.3375 4.77187 8.47187 4.77187 8.63437C4.77187 8.79375 4.825 8.92813 4.93125 9.0375L6.77187 10.8781L6.8625 10.95C6.96875 11.0219 7.07812 11.0562 7.18437 11.0562C7.39375 11.0531 7.55313 10.9656 7.65938 10.7844Z'
fill='#33B4FF'
/>
</svg>
</div>
)
}

View File

@@ -67,7 +67,7 @@ export function useOrganizations() {
* Fetch creator profile for a user
*/
async function fetchCreatorProfile(userId: string): Promise<CreatorProfile | null> {
const response = await fetch(`/api/creator-profiles?userId=${userId}`)
const response = await fetch(`/api/creators?userId=${userId}`)
// Treat 404 as "no profile"
if (response.status === 404) {
@@ -133,9 +133,7 @@ export function useSaveCreatorProfile() {
details: details && Object.keys(details).length > 0 ? details : undefined,
}
const url = existingProfileId
? `/api/creator-profiles/${existingProfileId}`
: '/api/creator-profiles'
const url = existingProfileId ? `/api/creators/${existingProfileId}` : '/api/creators'
const method = existingProfileId ? 'PUT' : 'POST'
const response = await fetch(url, {

View File

@@ -30,6 +30,7 @@ export interface TemplateCreator {
email?: string
website?: string
profileImageUrl?: string | null
verified?: boolean
details?: {
about?: string
xUrl?: string

View File

@@ -0,0 +1 @@
ALTER TABLE "template_creators" ADD COLUMN "verified" boolean DEFAULT false NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -799,6 +799,13 @@
"when": 1764468360258,
"tag": "0114_wise_sunfire",
"breakpoints": true
},
{
"idx": 115,
"version": "7",
"when": 1764477997303,
"tag": "0115_redundant_cerebro",
"breakpoints": true
}
]
}

View File

@@ -1335,6 +1335,7 @@ export const templateCreators = pgTable(
name: text('name').notNull(),
profileImageUrl: text('profile_image_url'),
details: jsonb('details'),
verified: boolean('verified').notNull().default(false),
createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),