diff --git a/sim/app/api/marketplace/[id]/info/route.ts b/sim/app/api/marketplace/[id]/info/route.ts index c3e7d95b0..3edab386a 100644 --- a/sim/app/api/marketplace/[id]/info/route.ts +++ b/sim/app/api/marketplace/[id]/info/route.ts @@ -42,7 +42,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ description: marketplaceEntry.description, category: marketplaceEntry.category, authorName: marketplaceEntry.authorName, - stars: marketplaceEntry.stars, views: marketplaceEntry.views, createdAt: marketplaceEntry.createdAt, updatedAt: marketplaceEntry.updatedAt, diff --git a/sim/app/api/marketplace/[id]/unpublish/route.ts b/sim/app/api/marketplace/[id]/unpublish/route.ts index f636238d5..3bc1ec186 100644 --- a/sim/app/api/marketplace/[id]/unpublish/route.ts +++ b/sim/app/api/marketplace/[id]/unpublish/route.ts @@ -1,54 +1,108 @@ import { NextRequest } from 'next/server' import { eq } from 'drizzle-orm' import { createLogger } from '@/lib/logs/console-logger' -import { validateWorkflowAccess } from '@/app/api/workflows/middleware' import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils' +import { getSession } from '@/lib/auth' import { db } from '@/db' import * as schema from '@/db/schema' const logger = createLogger('MarketplaceUnpublishAPI') +/** + * API endpoint to unpublish a workflow from the marketplace by its marketplace ID + * + * Security: + * - Requires authentication + * - Validates that the current user is the author of the marketplace entry + * - Only allows the owner to unpublish + */ export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { const requestId = crypto.randomUUID().slice(0, 8) try { const { id } = await params - - // Validate access to the workflow (must be owner to unpublish) - // Pass false to requireDeployment since unpublishing doesn't require the workflow to be deployed - const validation = await validateWorkflowAccess(request, id, false) - if (validation.error) { - logger.warn(`[${requestId}] Workflow access validation failed: ${validation.error.message}`) - return createErrorResponse(validation.error.message, validation.error.status) + + // Get the session first for authorization + const session = await getSession() + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized unpublish attempt for marketplace ID: ${id}`) + return createErrorResponse('Unauthorized', 401) } - // Check if workflow is published + const userId = session.user.id + + // Get the marketplace entry using the marketplace ID const marketplaceEntry = await db - .select() + .select({ + id: schema.marketplace.id, + workflowId: schema.marketplace.workflowId, + authorId: schema.marketplace.authorId, + name: schema.marketplace.name, + }) .from(schema.marketplace) - .where(eq(schema.marketplace.workflowId, id)) + .where(eq(schema.marketplace.id, id)) .limit(1) .then((rows) => rows[0]) if (!marketplaceEntry) { - logger.warn(`[${requestId}] No marketplace entry found for workflow: ${id}`) - return createErrorResponse('Workflow is not published to marketplace', 404) + logger.warn(`[${requestId}] No marketplace entry found with ID: ${id}`) + return createErrorResponse('Marketplace entry not found', 404) + } + + // Check if the user is the author of the marketplace entry + if (marketplaceEntry.authorId !== userId) { + logger.warn( + `[${requestId}] User ${userId} tried to unpublish marketplace entry they don't own: ${id}, author: ${marketplaceEntry.authorId}` + ) + return createErrorResponse('You do not have permission to unpublish this workflow', 403) } - // Delete the marketplace entry - await db.delete(schema.marketplace).where(eq(schema.marketplace.workflowId, id)) - - // Update the workflow to mark it as unpublished - await db.update(schema.workflow).set({ isPublished: false }).where(eq(schema.workflow.id, id)) - - logger.info(`[${requestId}] Workflow unpublished from marketplace: ${id}`) - - return createSuccessResponse({ - success: true, - message: 'Workflow successfully unpublished from marketplace', - }) + const workflowId = marketplaceEntry.workflowId + + // Verify the workflow exists and belongs to the user + const workflow = await db + .select({ + id: schema.workflow.id, + userId: schema.workflow.userId, + }) + .from(schema.workflow) + .where(eq(schema.workflow.id, workflowId)) + .limit(1) + .then((rows) => rows[0]) + + if (!workflow) { + logger.warn(`[${requestId}] Associated workflow not found: ${workflowId}`) + // We'll still delete the marketplace entry even if the workflow is missing + } else if (workflow.userId !== userId) { + logger.warn( + `[${requestId}] Workflow ${workflowId} belongs to user ${workflow.userId}, not current user ${userId}` + ) + return createErrorResponse('You do not have permission to unpublish this workflow', 403) + } + + try { + // Delete the marketplace entry - this is the primary action + await db.delete(schema.marketplace).where(eq(schema.marketplace.id, id)) + + // Update the workflow to mark it as unpublished if it exists + if (workflow) { + await db.update(schema.workflow) + .set({ isPublished: false }) + .where(eq(schema.workflow.id, workflowId)) + } + + logger.info(`[${requestId}] Workflow "${marketplaceEntry.name}" unpublished from marketplace: ID=${id}, workflowId=${workflowId}`) + + return createSuccessResponse({ + success: true, + message: 'Workflow successfully unpublished from marketplace', + }) + } catch (dbError) { + logger.error(`[${requestId}] Database error unpublishing marketplace entry:`, dbError) + return createErrorResponse('Failed to unpublish workflow due to a database error', 500) + } } catch (error) { - logger.error(`[${requestId}] Error unpublishing workflow: ${(await params).id}`, error) + logger.error(`[${requestId}] Error unpublishing marketplace entry: ${(await params).id}`, error) return createErrorResponse('Failed to unpublish workflow', 500) } } diff --git a/sim/app/api/marketplace/publish/route.ts b/sim/app/api/marketplace/publish/route.ts index f13accfa4..0bae30342 100644 --- a/sim/app/api/marketplace/publish/route.ts +++ b/sim/app/api/marketplace/publish/route.ts @@ -112,7 +112,6 @@ export async function POST(request: NextRequest) { .values({ ...marketplaceEntry, createdAt: new Date(), - stars: 0, views: 0, }) .returning() diff --git a/sim/app/api/marketplace/workflows/route.ts b/sim/app/api/marketplace/workflows/route.ts index 4aa2c9c9f..a55710c36 100644 --- a/sim/app/api/marketplace/workflows/route.ts +++ b/sim/app/api/marketplace/workflows/route.ts @@ -57,7 +57,6 @@ export async function GET(request: NextRequest) { authorId: schema.marketplace.authorId, authorName: schema.marketplace.authorName, state: schema.marketplace.state, - stars: schema.marketplace.stars, views: schema.marketplace.views, category: schema.marketplace.category, createdAt: schema.marketplace.createdAt, @@ -77,7 +76,6 @@ export async function GET(request: NextRequest) { description: schema.marketplace.description, authorId: schema.marketplace.authorId, authorName: schema.marketplace.authorName, - stars: schema.marketplace.stars, views: schema.marketplace.views, category: schema.marketplace.category, createdAt: schema.marketplace.createdAt, @@ -123,7 +121,6 @@ export async function GET(request: NextRequest) { authorId: schema.marketplace.authorId, authorName: schema.marketplace.authorName, state: schema.marketplace.state, - stars: schema.marketplace.stars, views: schema.marketplace.views, category: schema.marketplace.category, createdAt: schema.marketplace.createdAt, @@ -143,7 +140,6 @@ export async function GET(request: NextRequest) { description: schema.marketplace.description, authorId: schema.marketplace.authorId, authorName: schema.marketplace.authorName, - stars: schema.marketplace.stars, views: schema.marketplace.views, category: schema.marketplace.category, createdAt: schema.marketplace.createdAt, @@ -192,7 +188,6 @@ export async function GET(request: NextRequest) { name: schema.marketplace.name, description: schema.marketplace.description, authorName: schema.marketplace.authorName, - stars: schema.marketplace.stars, views: schema.marketplace.views, category: schema.marketplace.category, createdAt: schema.marketplace.createdAt, @@ -212,7 +207,7 @@ export async function GET(request: NextRequest) { result.popular = await db .select(selectFields) .from(schema.marketplace) - .orderBy(desc(schema.marketplace.stars), desc(schema.marketplace.views)) + .orderBy(desc(schema.marketplace.views)) .limit(limit) } @@ -262,7 +257,7 @@ export async function GET(request: NextRequest) { .select(selectFields) .from(schema.marketplace) .where(eq(schema.marketplace.category, categoryValue)) - .orderBy(desc(schema.marketplace.stars), desc(schema.marketplace.views)) + .orderBy(desc(schema.marketplace.views)) .limit(limit) // Always add the category to the result, even if empty @@ -277,15 +272,17 @@ export async function GET(request: NextRequest) { // Transform the data if state was included to match the expected format if (includeState) { const transformSection = (section: any[]) => { - return section.map((item) => - 'state' in item - ? { - ...item, - workflowState: item.state, - state: undefined, - } - : item - ) + return section.map((item) => { + if ('state' in item) { + // Create a new object without the state field, but with workflowState + const { state, ...rest } = item; + return { + ...rest, + workflowState: state + }; + } + return item; + }); } if (result.popular.length > 0) { diff --git a/sim/app/api/workflows/sync/route.ts b/sim/app/api/workflows/sync/route.ts index d3b94591b..fab2087a6 100644 --- a/sim/app/api/workflows/sync/route.ts +++ b/sim/app/api/workflows/sync/route.ts @@ -11,7 +11,7 @@ const logger = createLogger('WorkflowAPI') // Define marketplace data schema const MarketplaceDataSchema = z.object({ id: z.string(), - status: z.enum(['owner', 'temp', 'star']) + status: z.enum(['owner', 'temp']) }).nullable().optional() // Schema for workflow data diff --git a/sim/app/w/[id]/components/control-bar/components/marketplace-modal/marketplace-modal.tsx b/sim/app/w/[id]/components/control-bar/components/marketplace-modal/marketplace-modal.tsx index bdbe856b1..1d4ba10b2 100644 --- a/sim/app/w/[id]/components/control-bar/components/marketplace-modal/marketplace-modal.tsx +++ b/sim/app/w/[id]/components/control-bar/components/marketplace-modal/marketplace-modal.tsx @@ -17,7 +17,6 @@ import { LineChart, MailIcon, NotebookPen, - Star, Store, TimerIcon, Trash, @@ -38,6 +37,7 @@ import { } from '@/components/ui/form' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { LoadingAgent } from '@/components/ui/loading-agent' import { Select, SelectContent, @@ -131,7 +131,6 @@ interface MarketplaceInfo { description: string category: string authorName: string - stars: number views: number createdAt: string updatedAt: string @@ -183,14 +182,25 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps) try { setIsLoading(true) - const response = await fetch(`/api/marketplace/${activeWorkflowId}/info`) + + // Get marketplace ID from the workflow's marketplaceData + const marketplaceData = getMarketplaceData() + if (!marketplaceData?.id) { + throw new Error('No marketplace ID found in workflow data') + } + + // Use the marketplace ID to fetch details instead of workflow ID + const response = await fetch( + `/api/marketplace/workflows?marketplaceId=${marketplaceData.id}` + ) if (!response.ok) { throw new Error('Failed to fetch marketplace information') } - const data = await response.json() - setMarketplaceInfo(data) + // The API returns the data directly without wrapping + const marketplaceEntry = await response.json() + setMarketplaceInfo(marketplaceEntry) } catch (error) { console.error('Error fetching marketplace info:', error) addNotification('error', 'Failed to fetch marketplace information', activeWorkflowId) @@ -270,9 +280,13 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps) throw new Error(errorData.error || 'Failed to publish workflow') } + // Get the marketplace ID from the response + const responseData = await response.json() + const marketplaceId = responseData.data.id + // Update the marketplace data in the workflow registry updateWorkflow(activeWorkflowId, { - marketplaceData: { id: activeWorkflowId, status: 'owner' }, + marketplaceData: { id: marketplaceId, status: 'owner' }, }) // Add a marketplace notification with detailed information @@ -301,29 +315,46 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps) try { setIsUnpublishing(true) - const response = await fetch(`/api/marketplace/${activeWorkflowId}/unpublish`, { + // Get marketplace ID from the workflow's marketplaceData + const marketplaceData = getMarketplaceData() + if (!marketplaceData?.id) { + throw new Error('No marketplace ID found in workflow data') + } + + logger.info('Attempting to unpublish marketplace entry', { + marketplaceId: marketplaceData.id, + workflowId: activeWorkflowId, + status: marketplaceData.status, + }) + + const response = await fetch(`/api/marketplace/${marketplaceData.id}/unpublish`, { method: 'POST', }) if (!response.ok) { const errorData = await response.json() + logger.error('Error response from unpublish endpoint', { + status: response.status, + data: errorData, + }) throw new Error(errorData.error || 'Failed to unpublish workflow') } - // Remove the marketplace data from the workflow registry - updateWorkflow(activeWorkflowId, { - marketplaceData: null, + logger.info('Successfully unpublished workflow from marketplace', { + marketplaceId: marketplaceData.id, + workflowId: activeWorkflowId, }) - // Add a notification - addNotification( - 'marketplace', - `"${marketplaceInfo?.name || 'Workflow'}" successfully unpublished from marketplace`, - activeWorkflowId - ) - - // Close the modal after successful unpublishing + // First close the modal to prevent any flashing onOpenChange(false) + + // Then update the workflow state after modal is closed + setTimeout(() => { + // Remove the marketplace data from the workflow registry + updateWorkflow(activeWorkflowId, { + marketplaceData: null, + }) + }, 100) } catch (error: any) { console.error('Error unpublishing workflow:', error) addNotification('error', `Failed to unpublish workflow: ${error.message}`, activeWorkflowId) @@ -350,13 +381,8 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps) const renderMarketplaceInfo = () => { if (isLoading) { return ( -
Loading marketplace information...
-