Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
import { getGroupOrThrow } from "@/lib/api/groups/get-group-or-throw";
import { DubApiError } from "@/lib/api/errors";
import { withPartnerProfile } from "@/lib/auth/partner";
import { prisma } from "@/lib/prisma";
import { PartnerProgramGroupSchema } from "@/lib/zod/schemas/groups";
import { NextResponse } from "next/server";

// GET /api/partner-profile/programs/[programId]/groups/[groupIdOrSlug] - get information about a program's group
export const GET = withPartnerProfile(async ({ params }) => {
export const GET = withPartnerProfile(async ({ params, partner }) => {
const { programId, groupIdOrSlug } = params;

const group = await getGroupOrThrow({
programId,
groupId: groupIdOrSlug,
const group = await prisma.partnerGroup.findUnique({
where: {
...(groupIdOrSlug.startsWith("grp_")
? {
id: groupIdOrSlug,
}
: {
programId_slug: {
programId,
slug: groupIdOrSlug,
},
}),
programId,
partners: {
some: {
partnerId: partner.id,
},
},
},
});

if (!group) {
throw new DubApiError({
code: "not_found",
message: `Group "${groupIdOrSlug}" not found.`,
});
}

return NextResponse.json(PartnerProgramGroupSchema.parse(group));
});

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,13 @@ import { BackLink } from "@/ui/shared/back-link";
import { TokenAvatar } from "@/ui/token-avatar";
import { EmptyState, Webhook } from "@dub/ui";
import { fetcher } from "@dub/utils";
import { redirect, useRouter } from "next/navigation";
import { redirect, useParams, useRouter } from "next/navigation";
import { useState } from "react";
import useSWR from "swr";

interface PostbackDetailPageClientProps {
postbackId: string;
}

export function PostbackDetailPageClient({
postbackId,
}: PostbackDetailPageClientProps) {
export default function PostbackDetailPage() {
const router = useRouter();
const { postbackId } = useParams();
const [openPopover, setOpenPopover] = useState(false);

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { Button } from "@dub/ui";
export function AddPostbackButton({ onClick }: { onClick: () => void }) {
return (
<Button
className="flex h-10 items-center justify-center whitespace-nowrap rounded-lg border px-4 text-sm"
text="Add Postback"
className="h-9"
text="Create Postback"
onClick={onClick}
aria-label="Add postback"
aria-label="Create postback"
/>
);
}
39 changes: 37 additions & 2 deletions apps/web/app/api/analytics/dashboard/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { redis } from "@/lib/upstash";
import { parseAnalyticsQuery } from "@/lib/zod/schemas/analytics";
import { DUB_DEMO_LINKS, DUB_WORKSPACE_ID, getSearchParams } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";
Expand Down Expand Up @@ -48,7 +49,12 @@ export const GET = async (req: Request) => {
},
select: {
id: true,
dashboard: true,
dashboard: {
select: {
id: true,
password: true,
},
},
projectId: true,
project: {
select: {
Expand Down Expand Up @@ -80,6 +86,8 @@ export const GET = async (req: Request) => {
});
}

await assertDashboardPassword(folder.dashboard);

workspace = folder.project;

if ("links" in folder && folder.links?.length) link = folder.links[0];
Expand All @@ -102,7 +110,12 @@ export const GET = async (req: Request) => {
},
select: {
id: true,
dashboard: true,
dashboard: {
select: {
id: true,
password: true,
},
},
projectId: true,
project: {
select: {
Expand All @@ -123,6 +136,8 @@ export const GET = async (req: Request) => {
});
}

await assertDashboardPassword(link.dashboard);

workspace = link.project;
}
}
Expand Down Expand Up @@ -185,3 +200,23 @@ export const GET = async (req: Request) => {
return handleAndReturnErrorResponse(error);
}
};

async function assertDashboardPassword(dashboard: {
id: string;
password: string | null;
}) {
if (!dashboard.password) {
return;
}

const cookiePassword = (await cookies()).get(
`dub_password_${dashboard.id}`,
)?.value;

if (cookiePassword !== dashboard.password) {
throw new DubApiError({
code: "unauthorized",
message: "This dashboard is password protected",
});
}
}
82 changes: 82 additions & 0 deletions apps/web/app/api/links/bulk/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { includeProgramEnrollment } from "@/lib/api/links/include-program-enroll
import { includeTags } from "@/lib/api/links/include-tags";
import { throwIfLinksUsageExceeded } from "@/lib/api/links/usage-checks";
import { checkIfLinksHaveFolders } from "@/lib/api/links/utils/check-if-links-have-folders";
import { checkIfLinksHaveProgramPartners } from "@/lib/api/links/utils/check-if-links-have-program-partners";
import { isRootDomainLinkKey } from "@/lib/api/links/utils/is-root-domain-link-key";
import { combineTagIds } from "@/lib/api/tags/combine-tag-ids";
import { parseRequestBody } from "@/lib/api/utils";
Expand Down Expand Up @@ -222,6 +223,55 @@ export const POST = withWorkspace(
});
}

if (checkIfLinksHaveProgramPartners(validLinks)) {
const partnerIds = [
...new Set(
validLinks.map((link) => link.partnerId).filter(Boolean) as string[],
),
];

const enrollments =
workspace.defaultProgramId && partnerIds.length > 0
? await prisma.programEnrollment.findMany({
where: {
programId: workspace.defaultProgramId,
partnerId: { in: partnerIds },
},
select: {
partnerId: true,
},
})
: [];

const enrolledPartnerIds = new Set(
enrollments.map(({ partnerId }) => partnerId),
);

validLinks = validLinks.filter((link) => {
if (link.programId && link.programId !== workspace.defaultProgramId) {
errorLinks.push({
error: `Invalid programId detected: ${link.programId}`,
code: "unprocessable_entity",
link,
});

return false;
}

if (link.partnerId && !enrolledPartnerIds.has(link.partnerId)) {
errorLinks.push({
error: `Invalid partnerId detected: ${link.partnerId}`,
code: "unprocessable_entity",
link,
});

return false;
}

return true;
});
}

if (checkIfLinksHaveWebhooks(validLinks)) {
if (workspace.plan === "free" || workspace.plan === "pro") {
throw new DubApiError({
Expand Down Expand Up @@ -425,6 +475,38 @@ export const PATCH = withWorkspace(
});
}

if (data.programId || data.partnerId) {
if (data.programId && data.programId !== workspace.defaultProgramId) {
throw new DubApiError({
code: "unprocessable_entity",
message: `Invalid programId detected: ${data.programId}`,
});
}

if (data.partnerId) {
const enrollment = workspace.defaultProgramId
? await prisma.programEnrollment.findUnique({
where: {
partnerId_programId: {
partnerId: data.partnerId,
programId: workspace.defaultProgramId,
},
},
select: {
partnerId: true,
},
})
: null;

if (!enrollment) {
throw new DubApiError({
code: "unprocessable_entity",
message: `Invalid partnerId detected: ${data.partnerId}`,
});
}
}
}

const processedLinks = await Promise.all(
links.map(async (link) =>
processLink({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ export default function CreateWebhookButton() {
return (
<Link href={`/${slug}/settings/webhooks/new`}>
<Button
className="flex h-10 items-center justify-center whitespace-nowrap rounded-lg border px-4 text-sm"
text="Create Webhook"
className="h-9"
text="Create webhook"
disabledTooltip={permissionsError}
aria-label="Create webhook"
/>
</Link>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function verifyPassword(_prevState: any, data: FormData) {
}

(await cookies()).set(`dub_password_${dashboardId}`, password, {
path: `/share/${dashboardId}`,
path: "/",
httpOnly: true,
secure: true,
});
Expand Down
6 changes: 5 additions & 1 deletion apps/web/lib/api/commissions/create-manual-commissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,14 +480,18 @@ async function recordEvents(args: RecordEventsArgs) {
const clickId = nanoid(16);
const clickedAt = new Date(finalLeadEventDate.getTime() - 5 * 60 * 1000);

// Record click event
// Record a dummy click event (start of the conversion funnel)
const clickEvent = recordClickZodSchema.parse({
timestamp: clickedAt.toISOString(),
identity_hash: targetCustomer.externalId || targetCustomer.id,
workspace_id: workspace.id,
click_id: clickId,
link_id: targetLink.id,
domain: targetLink.domain,
key: targetLink.key,
url: targetLink.url,
ip: "127.0.0.1",
country: targetCustomer.country,
continent: targetCustomer.country
? COUNTRIES_TO_CONTINENTS[targetCustomer.country.toUpperCase()] || ""
: "",
Expand Down
35 changes: 22 additions & 13 deletions apps/web/lib/api/links/process-link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,22 +436,27 @@ export async function processLink<T extends Record<string, any>>({
}
}

// Program validity checks
// only perform program validity checks if not bulk creation (we do that check separately in the route itself)
if (programId && !skipProgramChecks) {
if (!partnerId && !tenantId) {
return {
link: payload,
error:
"programId was passed but no valid partnerId or tenantId was provided.",
code: "unprocessable_entity",
};
}

const program = await prisma.program.findUnique({
where: { id: programId },
select: {
workspaceId: true,
defaultFolderId: true,
...(!partnerId && tenantId
? {
partners: {
where: {
tenantId,
},
},
}
: {}),
partners: {
where: {
...(partnerId ? { partnerId } : { tenantId }),
},
},
},
});

Expand All @@ -463,11 +468,15 @@ export async function processLink<T extends Record<string, any>>({
};
}

if (!partnerId) {
partnerId =
program?.partners?.length > 0 ? program.partners[0].partnerId : null;
if (!program.partners.length) {
return {
link: payload,
error: "Invalid partnerId or tenantId provided.",
code: "not_found",
};
}

partnerId = program.partners[0].partnerId;
defaultProgramFolderId = program.defaultFolderId;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const checkIfLinksHaveProgramPartners = (
links: { programId?: string | null; partnerId?: string | null }[],
) => links.some((link) => link.programId || link.partnerId);
Loading
Loading