Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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
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