Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cursor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dependencies": {
"@cursor/sdk": "^1.0.12",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "1.1.15",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
Expand Down
48 changes: 48 additions & 0 deletions apps/cursor/src/actions/delete-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use server";

import { revalidatePath } from "next/cache";
import { z } from "zod";
import { createClient } from "@/utils/supabase/server";
import { ActionError, authActionClient } from "./safe-action";

export const deletePluginAction = authActionClient
.metadata({
actionName: "delete-plugin",
})
.schema(
z.object({
id: z.string().uuid(),
}),
)
.action(async ({ parsedInput: { id }, ctx: { userId } }) => {
const supabase = await createClient();

const { data: existing, error: fetchError } = await supabase
.from("plugins")
.select("id, owner_id, slug, active")
.eq("id", id)
.single();

if (fetchError || !existing) {
throw new ActionError("Plugin not found.");
}

if (existing.owner_id !== userId) {
throw new ActionError("You do not have permission to delete this plugin.");
}

const { error } = await supabase
.from("plugins")
.delete()
.eq("id", id)
.eq("owner_id", userId);

if (error) {
throw new ActionError(`Failed to delete plugin: ${error.message}`);
}

revalidatePath("/");
revalidatePath("/admin/plugins");

return { slug: existing.slug };
});
57 changes: 57 additions & 0 deletions apps/cursor/src/actions/toggle-plugin-listing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use server";

import { revalidatePath } from "next/cache";
import { z } from "zod";
import { createClient } from "@/utils/supabase/server";
import { ActionError, authActionClient } from "./safe-action";

export const togglePluginListingAction = authActionClient
.metadata({
actionName: "toggle-plugin-listing",
})
.schema(
z.object({
id: z.string().uuid(),
active: z.boolean(),
}),
)
.action(async ({ parsedInput: { id, active }, ctx: { userId } }) => {
const supabase = await createClient();

const { data: existing, error: fetchError } = await supabase
.from("plugins")
.select("id, owner_id, slug, permanently_blocked")
.eq("id", id)
.single();

if (fetchError || !existing) {
throw new ActionError("Plugin not found.");
}

if (existing.owner_id !== userId) {
throw new ActionError("You do not have permission to update this plugin.");
}

if (active && existing.permanently_blocked) {
throw new ActionError(
"This plugin cannot be published. Contact support if you believe this is a mistake.",
);
}

const { data, error } = await supabase
.from("plugins")
.update({ active })
.eq("id", id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Publish ignores scan status

High Severity · Logic Bug

togglePluginListingAction sets active to true for owners with only a permanently_blocked check, so plugins still pending, scanning, flagged, or error can be published and appear in directory queries that filter on active.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5e1c31e. Configure here.

.eq("owner_id", userId)
.select("slug")
.single();

if (error) {
throw new ActionError(error.message);
}

revalidatePath("/");
revalidatePath(`/plugins/${data.slug}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check after update

Medium Severity · Logic Bug

After the listing update, the action only checks Supabase error and then uses data.slug for revalidatePath. If the update returns no row without an error, data can be null and revalidation throws at runtime.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5e1c31e. Configure here.


return data;
});
4 changes: 3 additions & 1 deletion apps/cursor/src/app/plugins/[slug]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { Suspense } from "react";
import { EditPluginForm } from "@/components/forms/edit-plugin-form";
import { PluginOwnerMenu } from "@/components/plugins/plugin-owner-menu";
import { Login } from "@/components/login";
import { getPluginBySlug } from "@/data/queries";
import { getSession } from "@/utils/supabase/auth";
Expand Down Expand Up @@ -41,8 +42,9 @@ export default async function Page({ params }: { params: Params }) {
return (
<div className="min-h-screen px-6 pt-24 md:pt-32 pb-32">
<div className="mx-auto w-full max-w-lg">
<div className="mb-10">
<div className="mb-10 flex items-start justify-between gap-4">
<h1 className="marketing-page-title mb-3">Edit Plugin</h1>
<PluginOwnerMenu plugin={plugin} />
</div>

<EditPluginForm data={plugin} />
Expand Down
6 changes: 6 additions & 0 deletions apps/cursor/src/components/plugins/plugin-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type PluginCardData = {
keywords?: string[];
installCount?: number;
verified?: boolean;
unpublished?: boolean;
href: string;
};

Expand Down Expand Up @@ -57,6 +58,11 @@ export function PluginCard({ plugin }: { plugin: PluginCardData }) {
<h3 className="flex min-w-0 items-center gap-1.5 truncate text-sm font-medium tracking-[0.005em] text-foreground">
<span className="truncate">{plugin.name}</span>
{plugin.verified && <VerifiedBadge size="sm" />}
{plugin.unpublished && (
<span className="shrink-0 rounded border border-border px-1.5 py-0.5 text-[10px] font-mono uppercase text-muted-foreground">
Unpublished
</span>
)}
</h3>
</div>

Expand Down
19 changes: 9 additions & 10 deletions apps/cursor/src/components/plugins/plugin-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
Copy,
Download,
Loader2,
Pencil,
ShieldAlert,
} from "lucide-react";
import Image from "next/image";
Expand All @@ -23,6 +22,7 @@ import { createClient } from "@/utils/supabase/client";
import { PluginIconFallback } from "./plugin-icon";
import { StarButton } from "./star-button";
import { VerifiedBadge } from "./verified-badge";
import { PluginOwnerMenu } from "./plugin-owner-menu";
import { VerifyControls } from "./verify-controls";

function isValidImageUrl(url: string | null): url is string {
Expand Down Expand Up @@ -255,6 +255,13 @@ export function PluginDetailView({ plugin }: { plugin: PluginRow }) {
<div className="min-h-screen px-4 pt-24 md:pt-32">
<div className="page-shell max-w-4xl px-0 py-8">
<ScanStatusBanner plugin={plugin} isOwner={isOwner} />
{isOwner && !plugin.active && (
<div className="mb-6 flex items-center gap-2 rounded-lg border border-border bg-muted/40 px-4 py-3">
<span className="text-sm text-muted-foreground">
This plugin is unpublished and hidden from the directory.
</span>
</div>
)}
<div className="mb-6 flex items-center gap-4">
<PluginLogo logo={plugin.logo} name={plugin.name} size={40} />
<div className="flex-1">
Expand All @@ -267,19 +274,11 @@ export function PluginDetailView({ plugin }: { plugin: PluginRow }) {
</div>
<div className="flex items-center gap-2">
<VerifyControls plugin={plugin} />
<PluginOwnerMenu plugin={plugin} />
<span className="flex items-center gap-1.5 rounded-full border border-border bg-card px-3 py-1.5 text-sm text-muted-foreground">
<Download className="size-3.5" />
<span className="text-xs">{formatCount(installCount)}</span>
</span>
{isOwner && (
<Link
href={`/plugins/${plugin.slug}/edit`}
className="flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<Pencil className="size-3.5" />
Edit
</Link>
)}
<StarButton
pluginId={plugin.id}
slug={plugin.slug}
Expand Down
Loading