Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
75 changes: 40 additions & 35 deletions apps/web/app/(ee)/api/admin/partners/[partnerId]/platforms/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ export const POST = withAdmin(
} = postSchema.parse(await req.json());

const partner = await prisma.partner.findUnique({
where: { id: partnerId },
select: { id: true },
where: {
id: partnerId,
},
select: {
id: true,
},
});

if (!partner) {
Expand Down Expand Up @@ -86,47 +90,48 @@ export const POST = withAdmin(
};
}

const updated = await prisma.partnerPlatform.upsert({
const existingPlatform = await prisma.partnerPlatform.findFirst({
where: {
partnerId_type: {
partnerId,
type: platform,
},
},
create: {
partnerId,
type: platform,
identifier,
verifiedAt: new Date(),
platformId: verifiedData.platformId,
subscribers: verifiedData.subscribers,
posts: verifiedData.posts,
views: verifiedData.views,
avatarUrl: verifiedData.avatarUrl,
metadata: {
...(verifiedData.metadata ?? {}),
manuallyVerifiedByAdmin: true,
manuallyVerifiedAt: new Date().toISOString(),
},
lastCheckedAt: new Date(),
},
update: {
identifier,
verifiedAt: new Date(),
platformId: verifiedData.platformId,
subscribers: verifiedData.subscribers,
posts: verifiedData.posts,
views: verifiedData.views,
avatarUrl: verifiedData.avatarUrl,
metadata: {
...(verifiedData.metadata ?? {}),
manuallyVerifiedByAdmin: true,
manuallyVerifiedAt: new Date().toISOString(),
},
lastCheckedAt: new Date(),
select: {
id: true,
},
});

const data = {
verifiedAt: new Date(),
platformId: verifiedData.platformId,
subscribers: verifiedData.subscribers,
posts: verifiedData.posts,
views: verifiedData.views,
avatarUrl: verifiedData.avatarUrl,
metadata: {
...(verifiedData.metadata ?? {}),
manuallyVerifiedByAdmin: true,
manuallyVerifiedAt: new Date().toISOString(),
},
lastCheckedAt: new Date(),
};

const updated = existingPlatform
? await prisma.partnerPlatform.update({
where: {
id: existingPlatform.id,
},
data,
})
: await prisma.partnerPlatform.create({
data: {
partnerId,
type: platform,
identifier,
...data,
},
});

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return NextResponse.json({
platform: {
...updated,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,14 @@ export const GET = withAdmin(async ({ params }) => {
(platform) => platform.verifiedAt !== null,
);

// a partner has at most one platform per type, so there is at most one website
const websitePlatform = verifiedPlatforms.find(
(platform) => platform.type === "website",
);

const websiteDomain = websitePlatform
? getDomainWithoutWWW(websitePlatform.identifier) ?? null
: null;

const matchConditions: Prisma.PartnerPlatformWhereInput[] = [];

for (const platform of verifiedPlatforms) {
if (platform.type === "website") {
// website identifiers are full URLs with inconsistent normalization,
// so match on the domain instead of the exact identifier
const websiteDomain = getDomainWithoutWWW(platform.identifier);

if (websiteDomain) {
matchConditions.push({
identifier: {
Expand Down Expand Up @@ -88,18 +81,25 @@ export const GET = withAdmin(async ({ params }) => {

const sharedPlatforms = verifiedPlatforms
.map((platform) => {
let platformMatches = sharedPlatformMatches.filter(
(match) => match.type === platform.type,
);
let platformMatches: typeof sharedPlatformMatches;

// "contains" matches on the domain need exact verification
// (e.g. contains "example.com" would also match "notexample.com")
if (platform.type === "website") {
platformMatches = platformMatches.filter(
// "contains" matches on the domain need exact verification
// (e.g. contains "example.com" would also match "notexample.com")
const websiteDomain = getDomainWithoutWWW(platform.identifier);

platformMatches = sharedPlatformMatches.filter(
(match) =>
match.type === platform.type &&
websiteDomain !== null &&
getDomainWithoutWWW(match.identifier) === websiteDomain,
);
} else {
platformMatches = sharedPlatformMatches.filter(
(match) =>
match.type === platform.type &&
match.identifier === platform.identifier,
);
Comment on lines +87 to +105

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t apply the global take: 10 before exact website filtering.

Website candidates are fetched with broad contains matching, then narrowed here. Because the DB query already applies take: 10, false positives like notexample.com can consume the limit before Line 91 filters them out, causing real shared platforms to be omitted. Move the limit after exact normalization, or query/filter per platform so each row gets complete exact matches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(ee)/api/admin/partners/[partnerId]/shared-platforms/route.ts
around lines 84 - 102, The shared-platform lookup in the route handler is
applying the `take: 10` limit too early, so broad `contains` website candidates
can crowd out exact matches before `platformMatches` does its
normalization/filtering. Update the `sharedPlatformMatches` query and the
`platformMatches` filtering logic so the limit is applied only after exact
website domain normalization (or filter per platform first), ensuring
`getDomainWithoutWWW`-based website matching does not lose valid results.

}

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { withCron } from "@/lib/cron/with-cron";
import { prisma } from "@/lib/prisma";
import { MAX_PLATFORMS_PER_TYPE } from "@/lib/social-utils";
import { logAndRespond } from "../../utils";

export const dynamic = "force-dynamic";

/**
* Safety-net cron that enforces the per-type handle limit in case the
* application-level guard is ever bypassed (there is no DB-level unique key).
* For any (partnerId, type) group over the limit, it keeps the handles to
* retain — verified first, then oldest — and deletes the most-recently-added
* excess.
*
* Runs daily. GET /api/cron/partner-platforms/enforce-limits
*/
export const GET = withCron(async () => {
const overLimitGroups = await prisma.partnerPlatform.groupBy({
by: ["partnerId", "type"],
_count: {
id: true,
},
having: {
id: {
_count: {
gt: MAX_PLATFORMS_PER_TYPE,
},
},
},
orderBy: {
partnerId: "asc",
},
take: 100,
});

if (overLimitGroups.length === 0) {
return logAndRespond("No partners over the per-type platform limit.");
}

let trimmedRows = 0;

for (const { partnerId, type } of overLimitGroups) {
const platforms = await prisma.partnerPlatform.findMany({
where: {
partnerId,
type,
},
select: {
id: true,
verifiedAt: true,
createdAt: true,
},
});

// Retain verified handles first, then the oldest; delete the rest
const sortedPlatforms = platforms.sort((a, b) => {
const aVerified = a.verifiedAt ? 1 : 0;
const bVerified = b.verifiedAt ? 1 : 0;

if (aVerified !== bVerified) {
return bVerified - aVerified;
}

return a.createdAt.getTime() - b.createdAt.getTime();
});

const excessIds = sortedPlatforms
.slice(MAX_PLATFORMS_PER_TYPE)
.map((r) => r.id);

if (excessIds.length > 0) {
await prisma.partnerPlatform.deleteMany({
where: {
id: {
in: excessIds,
},
},
});

trimmedRows += excessIds.length;

console.log(
`Trimmed ${excessIds.length} excess ${type} handle(s) for partner ${partnerId}.`,
);
}
}

return logAndRespond(
`Enforced per-type platform limit: trimmed ${trimmedRows} row(s) across ${overLimitGroups.length} group(s).`,
);
});
17 changes: 7 additions & 10 deletions apps/web/app/(ee)/api/partners/platforms/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const requestSchema = z.object({
interface State {
platform: PlatformType;
partnerId: string;
identifier: string;
source: "onboarding" | "settings";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

Expand Down Expand Up @@ -56,7 +57,7 @@ export async function GET(req: Request) {
return NextResponse.redirect(PARTNERS_DOMAIN);
}

const { platform, partnerId, source } = stateFromRedis;
const { platform, partnerId, identifier, source } = stateFromRedis;

if (session.user.defaultPartnerId !== partnerId) {
console.warn("Unauthorized: User is not the default partner.");
Expand Down Expand Up @@ -125,12 +126,11 @@ export async function GET(req: Request) {
return NextResponse.redirect(redirectUrl);
}

const partnerPlatform = await prisma.partnerPlatform.findUnique({
const partnerPlatform = await prisma.partnerPlatform.findFirst({
where: {
partnerId_type: {
partnerId,
type: platform,
},
partnerId,
type: platform,
identifier,
},
});

Expand Down Expand Up @@ -174,10 +174,7 @@ export async function GET(req: Request) {

await prisma.partnerPlatform.update({
where: {
partnerId_type: {
partnerId,
type: platform,
},
id: partnerPlatform.id,
},
data: {
...socialStats,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,17 @@ export function EmbedSocialUrlField({
bounty,
value,
onChange,
partnerPlatform,
partnerPlatforms,
onVerifyingChange,
onRequirementsMetChange,
}: {
bounty: PartnerBountyProps;
value: string;
onChange: (v: string) => void;
partnerPlatform?: Pick<PartnerPlatformProps, "identifier" | "verifiedAt">;
partnerPlatforms?: Pick<
PartnerPlatformProps,
"type" | "identifier" | "verifiedAt"
>[];
onVerifyingChange: (value: boolean) => void;
onRequirementsMetChange: (value: boolean) => void;
}) {
Expand Down Expand Up @@ -245,15 +248,8 @@ export function EmbedSocialUrlField({
const { isPostedFromYourAccount, isAfterStartDate } =
evaluateSocialContentRequirements({
content: data,
bounty: {
startsAt: new Date(bounty.startsAt),
},
partnerPlatform: {
identifier: partnerPlatform?.identifier ?? "",
verifiedAt: partnerPlatform?.verifiedAt
? new Date(partnerPlatform.verifiedAt)
: null,
},
bounty,
partnerPlatforms,
});

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import { getPeriodLabel } from "@/lib/bounty/periods";
import { resolveBountyDetails } from "@/lib/bounty/utils";
import { PartnerBountyProps, PartnerBountySubmission } from "@/lib/types";
import {
PartnerBountyProps,
PartnerBountySubmission,
PartnerPlatformProps,
} from "@/lib/types";
import { SocialAccountNotVerifiedWarning } from "@/ui/partners/bounties/bounty-social-content";
import { Button, ChevronRight, Popover, Trophy } from "@dub/ui";
import { PlatformType } from "@prisma/client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { toast } from "sonner";
Expand Down Expand Up @@ -75,11 +78,10 @@ export function EmbedBountySubmissionForm({
onSuccess,
}: {
bounty: PartnerBountyProps;
partnerPlatforms: Array<{
type: PlatformType;
identifier: string;
verifiedAt: Date | null;
}>;
partnerPlatforms: Pick<
PartnerPlatformProps,
"type" | "identifier" | "verifiedAt"
>[];
periodNumber: number;
existingSubmission?: PartnerBountySubmission | null;
onCancel: () => void;
Expand All @@ -90,9 +92,6 @@ export function EmbedBountySubmissionForm({
const token = useEmbedToken();
const bountyInfo = resolveBountyDetails(bounty);
const isSocialMetricsBounty = bountyInfo?.hasSocialMetrics ?? false;
const partnerPlatform = bountyInfo?.socialPlatform
? partnerPlatforms.find((p) => p.type === bountyInfo.socialPlatform?.value)
: undefined;
const imageRequired = !!bounty.submissionRequirements?.image;
const urlRequired =
!!bounty.submissionRequirements?.url && !isSocialMetricsBounty;
Expand Down Expand Up @@ -205,6 +204,8 @@ export function EmbedBountySubmissionForm({
}
};

const hasVerifiedPlatform = partnerPlatforms.some((p) => p.verifiedAt);

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return (
<div className="border-border-subtle bg-bg-default overflow-hidden rounded-xl border">
<SubmissionCardHeader
Expand Down Expand Up @@ -303,14 +304,14 @@ export function EmbedBountySubmissionForm({

{isSocialMetricsBounty && bountyInfo?.socialPlatform ? (
<>
{!partnerPlatform?.verifiedAt && (
{!hasVerifiedPlatform && (
<SocialAccountNotVerifiedWarning bounty={bounty} />
)}
<EmbedSocialUrlField
bounty={bounty}
value={urls[0] ?? ""}
onChange={(v) => setUrls([v])}
partnerPlatform={partnerPlatform}
partnerPlatforms={partnerPlatforms}
onVerifyingChange={setSocialContentVerifying}
onRequirementsMetChange={setSocialContentRequirementsMet}
/>
Expand Down
Loading
Loading