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
55 changes: 25 additions & 30 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,45 +90,36 @@ export const POST = withAdmin(
};
}

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 = await prisma.partnerPlatform.upsert({
where: {
partnerId_type: {
partnerId_type_identifier: {
partnerId,
type: platform,
identifier,
},
},
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(),
...data,
},
update: data,
});

return NextResponse.json({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export const GET = withAdmin(async ({ params }) => {
const { partnerId } = params;

const partner = await prisma.partner.findUnique({
where: { id: partnerId },
where: {
id: partnerId,
},
include: {
platforms: true,
},
Expand All @@ -20,25 +22,19 @@ export const GET = withAdmin(async ({ params }) => {
if (!partner) {
return new Response("Partner not found.", { status: 404 });
}

const verifiedPlatforms = partner.platforms.filter(
(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 @@ -83,23 +79,30 @@ export const GET = withAdmin(async ({ params }) => {
orderBy: {
createdAt: "asc",
},
take: 10,
take: 50,
});

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
34 changes: 22 additions & 12 deletions apps/web/app/(ee)/api/partners/platforms/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ const requestSchema = z.object({
state: z.string(),
});

interface State {
platform: PlatformType;
partnerId: string;
source: "onboarding" | "settings";
}
const stateSchema = z.object({
platform: z.enum(PlatformType),
partnerId: z.string(),
identifier: z.string(),
source: z.enum(["onboarding", "settings"]),
});

// GET /api/partners/platforms/callback
export async function GET(req: Request) {
Expand All @@ -47,7 +48,7 @@ export async function GET(req: Request) {
}

// Find the state from Redis
const stateFromRedis = await redis.get<State>(
const stateFromRedis = await redis.get<z.infer<typeof stateSchema>>(
`partnerSocialVerification:${state}`,
);

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

const { platform, partnerId, source } = stateFromRedis;
const parsedState = stateSchema.safeParse(stateFromRedis);

if (!parsedState.success) {
console.warn("State is invalid.");
return NextResponse.redirect(PARTNERS_DOMAIN);
}

const { platform, partnerId, identifier, source } = parsedState.data;

if (session.user.defaultPartnerId !== partnerId) {
console.warn("Unauthorized: User is not the default partner.");
Expand Down Expand Up @@ -127,11 +135,16 @@ export async function GET(req: Request) {

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

if (!partnerPlatform || !partnerPlatform.identifier) {
Expand Down Expand Up @@ -174,10 +187,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,10 @@ export function EmbedBountySubmissionForm({
}
};

const hasVerifiedPlatform = partnerPlatforms.some(
(p) => p.type === bountyInfo?.socialPlatform?.value && p.verifiedAt,
);

return (
<div className="border-border-subtle bg-bg-default overflow-hidden rounded-xl border">
<SubmissionCardHeader
Expand Down Expand Up @@ -303,14 +306,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
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { getSession } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { buildSocialPlatformLookup } from "@/lib/social-utils";
import { PartnerPlatformProps } from "@/lib/types";
import { buildSocialPlatformLookup, PLATFORM_ORDER } from "@/lib/social-utils";
import { partnerPlatformSchema } from "@/lib/zod/schemas/partners";
import { PartnerPlatformsForm } from "@/ui/partners/partner-platforms-form";
import { PlatformType } from "@prisma/client";
import { Suspense } from "react";
import * as z from "zod/v4";
import { OnboardingPlatformsPageClient } from "./page-client";
Expand Down Expand Up @@ -82,23 +80,12 @@ async function OnboardingPlatformsFormRSC() {
}

// Merge social handles from a partner application into the partner platforms list.
const platforms: PartnerPlatformProps[] = z
.array(partnerPlatformSchema)
.parse(partner.platforms);
const platforms = z.array(partnerPlatformSchema).parse(partner.platforms);

if (application) {
const socialPlatformsMap = buildSocialPlatformLookup(platforms);

const APPLICATION_SOCIAL_PLATFORMS = [
"website",
"youtube",
"twitter",
"linkedin",
"instagram",
"tiktok",
] as const satisfies readonly PlatformType[];

for (const platform of APPLICATION_SOCIAL_PLATFORMS) {
for (const platform of PLATFORM_ORDER) {
const handle = application[platform];

// Application-provided handles are added only if the partner does not already have that platform.
Expand Down
Loading
Loading