Skip to content
Closed
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
10 changes: 8 additions & 2 deletions apps/cursor/src/actions/track-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { revalidatePath } from "next/cache";
import { z } from "zod";
import { installGlobalLimit, installPerPluginLimit } from "@/lib/rate-limit";
import { createClient as createAdminClient } from "@/utils/supabase/admin-client";
import { ActionError, actionClient } from "./safe-action";
import { actionClient } from "./safe-action";

export type TrackInstallResult =
| { tracked: true }
| { tracked: false; rateLimited: true };

export const trackInstallAction = actionClient
.metadata({
Expand All @@ -23,7 +27,7 @@ export const trackInstallAction = actionClient
]);

if (!global.success || !perPlugin.success) {
throw new ActionError("Rate limit exceeded. Please try again later.");
return { tracked: false, rateLimited: true } satisfies TrackInstallResult;
}

const admin = await createAdminClient();
Expand All @@ -34,4 +38,6 @@ export const trackInstallAction = actionClient

revalidatePath("/");
revalidatePath(`/plugins/${slug}`);

return { tracked: true } satisfies TrackInstallResult;
});
99 changes: 77 additions & 22 deletions apps/cursor/src/actions/upsert-company.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { z } from "zod";
import { createClient } from "@/utils/supabase/server";
import { ActionError, authActionClient } from "./safe-action";

// Postgres unique_violation. Raised when an insert/update collides with the
// case-insensitive company name index (companies_name_key_unique).
const UNIQUE_VIOLATION = "23505";

export const upsertCompanyAction = authActionClient
.metadata({
actionName: "upsert-company",
Expand All @@ -27,7 +31,7 @@ export const upsertCompanyAction = authActionClient
async ({
parsedInput: {
id,
name,
name: rawName,
image,
slug,
location,
Expand All @@ -41,6 +45,9 @@ export const upsertCompanyAction = authActionClient
}) => {
const supabase = await createClient();

const name = rawName.trim();
const nameKey = name.toLowerCase();

// Only treat the request as an edit when a row with the provided id
// already exists. The form always generates a client-side nanoid for new
// companies, so the presence of `id` alone does not imply an edit.
Expand All @@ -51,36 +58,84 @@ export const upsertCompanyAction = authActionClient
.eq("id", id)
.maybeSingle();

if (existing && existing.owner_id !== userId) {
throw new ActionError(
"You don't have permission to edit this company",
);
if (existing) {
if (existing.owner_id !== userId) {
throw new ActionError(
"You don't have permission to edit this company",
);
}

const { data, error } = await supabase
.from("companies")
.update({
name,
image,
location,
bio,
website,
social_x_link,
public: is_public,
})
.eq("id", id)
.select("id, slug")
.single();

if (error) {
if (error.code === UNIQUE_VIOLATION) {
throw new ActionError("A company with this name already exists.");
}
throw new ActionError(error.message);
}

if (shouldRedirect) {
redirect(`/c/${data?.slug}`);
}

return data;
}
}

// New company. Insert directly so the case-insensitive unique index can
// reject duplicates even under concurrent/double submissions. The slug is
// assigned by the `generate_company_slug` trigger.
const { data, error } = await supabase
.from("companies")
.upsert(
{
id: id ?? undefined,
name,
image,
location,
slug: slug ?? undefined,
bio,
website,
social_x_link,
public: is_public,
owner_id: userId,
},
{
onConflict: "id",
},
)
.insert({
id: id ?? undefined,
name,
image,
location,
slug: slug ?? undefined,
bio,
website,
social_x_link,
public: is_public,
owner_id: userId,
})
.select("id, slug")
.single();

if (error) {
// A company with this name already exists. Reuse it instead of creating
// a duplicate so retries/double-clicks resolve to the same record.
if (error.code === UNIQUE_VIOLATION) {
const { data: existing } = await supabase
.from("companies")
.select("id, slug")
.eq("name_key", nameKey)
.maybeSingle();

if (existing) {
if (shouldRedirect) {
redirect(`/c/${existing.slug}`);
}

return existing;
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.

Reuses another user's company

Medium Severity · Logic Bug

When a new company insert hits a name unique violation, the handler returns the existing row by name_key without checking owner_id. A user creating a name already owned by someone else can succeed and be redirected to that other company's profile instead of receiving “A company with this name already exists.”

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6baf7c5. Configure here.

}

throw new ActionError("A company with this name already exists.");
}

throw new ActionError(error.message);
}

Expand Down
16 changes: 13 additions & 3 deletions apps/cursor/src/app/c/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { getCompanyProfile } from "@/data/queries";
import { createOGResponse, OG, OGLayout } from "@/lib/og";
import {
createOGResponse,
OG,
OGLayout,
resolveOgImageUrl,
} from "@/lib/og";

export const alt = "Company Profile";
export const size = { width: OG.width, height: OG.height };
export const contentType = "image/png";
export const revalidate = 86400;

export default async function Image({
params,
Expand All @@ -30,10 +36,12 @@ export default async function Image({
);
}

const logoUrl = resolveOgImageUrl(data.image);

return createOGResponse(
<OGLayout>
<div style={{ display: "flex", alignItems: "center", gap: 40 }}>
{data.image && (
{logoUrl && (
<div
style={{
display: "flex",
Expand All @@ -48,7 +56,7 @@ export default async function Image({
}}
>
<img
src={data.image}
src={logoUrl}
width={104}
height={104}
style={{ borderRadius: 14, objectFit: "contain" }}
Expand All @@ -66,6 +74,7 @@ export default async function Image({
>
<div
style={{
display: "flex",
fontSize: 52,
fontWeight: 700,
color: OG.text,
Expand Down Expand Up @@ -106,6 +115,7 @@ export default async function Image({
{data.bio && (
<div
style={{
display: "flex",
fontSize: 22,
color: OG.textTertiary,
lineHeight: 1.4,
Expand Down
1 change: 1 addition & 0 deletions apps/cursor/src/app/companies/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createListingOG, OG } from "@/lib/og";
export const alt = "Companies";
export const size = { width: OG.width, height: OG.height };
export const contentType = "image/png";
export const revalidate = 86400;

export default async function Image() {
return createListingOG("Companies", "Companies using Cursor");
Expand Down
1 change: 1 addition & 0 deletions apps/cursor/src/app/login/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createListingOG, OG } from "@/lib/og";
export const alt = "Sign In";
export const size = { width: OG.width, height: OG.height };
export const contentType = "image/png";
export const revalidate = 86400;

export default async function Image() {
return createListingOG("Sign In", "Sign in to Cursor Directory");
Expand Down
1 change: 1 addition & 0 deletions apps/cursor/src/app/members/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createListingOG, OG } from "@/lib/og";
export const alt = "Members";
export const size = { width: OG.width, height: OG.height };
export const contentType = "image/png";
export const revalidate = 86400;

export default async function Image() {
return createListingOG(
Expand Down
10 changes: 9 additions & 1 deletion apps/cursor/src/app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { CursorIcon, createOGResponse, OG, OGLayout } from "@/lib/og";
import {
CursorIcon,
createOGResponse,
OG,
OGLayout,
} from "@/lib/og";

export const alt = "Cursor Directory";
export const size = { width: OG.width, height: OG.height };
export const contentType = "image/png";
export const revalidate = 86400;

export default async function Image() {
return createOGResponse(
Expand All @@ -20,6 +26,7 @@ export default async function Image() {
<CursorIcon size={80} />
<div
style={{
display: "flex",
fontSize: 56,
fontWeight: 700,
color: OG.text,
Expand All @@ -31,6 +38,7 @@ export default async function Image() {
</div>
<div
style={{
display: "flex",
fontSize: 26,
color: OG.textSecondary,
lineHeight: 1.4,
Expand Down
28 changes: 22 additions & 6 deletions apps/cursor/src/app/plugins/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { getPluginBySlug } from "@/data/queries";
import {
CursorIcon,
createOGResponse,
formatCount,
OG,
OGLayout,
resolveOgImageUrl,
} from "@/lib/og";

export const alt = "Plugin";
export const size = { width: OG.width, height: OG.height };
export const contentType = "image/png";
// Must be a literal — Next.js segment config does not accept imported values.
export const revalidate = 86400;

export default async function Image({
params,
Expand All @@ -36,6 +38,8 @@ export default async function Image({
);
}

const logoUrl = resolveOgImageUrl(data.logo);

const components = data.plugin_components ?? [];
const typeCounts: Record<string, number> = {};
for (const c of components) {
Expand All @@ -62,16 +66,17 @@ export default async function Image({
padding: 6,
}}
>
{data.logo ? (
{logoUrl ? (
<img
src={data.logo}
src={logoUrl}
width={60}
height={60}
style={{ borderRadius: 10, objectFit: "contain" }}
/>
) : (
<span
style={{
display: "flex",
fontSize: 32,
fontWeight: 700,
color: OG.textSecondary,
Expand All @@ -90,6 +95,7 @@ export default async function Image({
>
<div
style={{
display: "flex",
fontSize: 48,
fontWeight: 700,
color: OG.text,
Expand All @@ -100,7 +106,13 @@ export default async function Image({
{data.name}
</div>
{data.author_name && (
<div style={{ fontSize: 22, color: OG.textSecondary }}>
<div
style={{
display: "flex",
fontSize: 22,
color: OG.textSecondary,
}}
>
by {data.author_name}
</div>
)}
Expand All @@ -110,6 +122,7 @@ export default async function Image({
{data.description && (
<div
style={{
display: "flex",
fontSize: 24,
color: OG.textSecondary,
lineHeight: 1.4,
Expand Down Expand Up @@ -146,13 +159,15 @@ export default async function Image({
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
<span style={{ fontWeight: 700 }}>
<span style={{ display: "flex", fontWeight: 700 }}>
{formatCount(data.install_count)}
</span>
</div>

{componentSummary && (
<div style={{ fontSize: 20, color: OG.textTertiary }}>
<div
style={{ display: "flex", fontSize: 20, color: OG.textTertiary }}
>
{componentSummary}
</div>
)}
Expand All @@ -164,6 +179,7 @@ export default async function Image({
<div
key={kw}
style={{
display: "flex",
fontSize: 16,
color: OG.textSecondary,
padding: "6px 14px",
Expand Down
1 change: 1 addition & 0 deletions apps/cursor/src/app/plugins/new/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createListingOG, OG } from "@/lib/og";
export const alt = "Submit a Plugin";
export const size = { width: OG.width, height: OG.height };
export const contentType = "image/png";
export const revalidate = 86400;

export default async function Image() {
return createListingOG(
Expand Down
1 change: 1 addition & 0 deletions apps/cursor/src/app/plugins/opengraph-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createListingOG, OG } from "@/lib/og";
export const alt = "Plugins";
export const size = { width: OG.width, height: OG.height };
export const contentType = "image/png";
export const revalidate = 86400;

export default async function Image() {
return createListingOG(
Expand Down
Loading