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
Expand Up @@ -105,7 +105,7 @@ export const POST = withCron(async ({ rawBody }) => {
links.map((link) => ({
queueName: "create-discount-code",
url: `${APP_DOMAIN_WITH_NGROK}/api/cron/discount-codes/create`,
deduplicationId: `${discountId}-${link.id}-1`,
deduplicationId: `${discountId}-${link.id}`,
body: {
linkId: link.id,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getPostbackOrThrow } from "@/lib/api/postbacks/get-postback-or-throw";
import { withPartnerProfile } from "@/lib/auth/partner";
import { getPostbackEvents } from "@/lib/postback/api/get-postback-events";
import { NextResponse } from "next/server";

// GET /api/partner-profile/postbacks/[postbackId]/events
export const GET = withPartnerProfile(
async ({ partner, params }) => {
const { postbackId } = params;

await getPostbackOrThrow({
postbackId,
partnerId: partner.id,
});

const events = await getPostbackEvents({
postbackId,
});

return NextResponse.json(events.data);
},
{
requiredPermission: "postbacks.read",
featureFlag: "postbacks",
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createToken } from "@/lib/api/oauth/utils";
import { getPostbackOrThrow } from "@/lib/api/postbacks/get-postback-or-throw";
import { withPartnerProfile } from "@/lib/auth/partner";
import {
POSTBACK_SECRET_LENGTH,
POSTBACK_SECRET_PREFIX,
} from "@/lib/postback/constants";
import { prisma } from "@dub/prisma";
import { NextResponse } from "next/server";

// POST /api/partner-profile/postbacks/[postbackId]/rotate-secret
export const POST = withPartnerProfile(
async ({ partner, params }) => {
const { postbackId } = params;

await getPostbackOrThrow({
postbackId,
partnerId: partner.id,
});

const secret = createToken({
prefix: POSTBACK_SECRET_PREFIX,
length: POSTBACK_SECRET_LENGTH,
});

await prisma.postback.update({
where: {
id: postbackId,
},
data: {
secret,
},
});

return NextResponse.json({ secret });
},
{
requiredPermission: "postbacks.write",
featureFlag: "postbacks",
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { getPostbackOrThrow } from "@/lib/api/postbacks/get-postback-or-throw";
import { parseRequestBody } from "@/lib/api/utils";
import { withPartnerProfile } from "@/lib/auth/partner";
import {
postbackSchema,
updatePostbackInputSchema,
} from "@/lib/postback/schemas";
import { prisma } from "@dub/prisma";
import { NextResponse } from "next/server";

// GET /api/partner-profile/postbacks/[postbackId]
export const GET = withPartnerProfile(
async ({ partner, params }) => {
const { postbackId } = params;

const postback = await getPostbackOrThrow({
postbackId,
partnerId: partner.id,
});

return NextResponse.json(postbackSchema.parse(postback));
},
{
requiredPermission: "postbacks.read",
featureFlag: "postbacks",
},
);

// PATCH /api/partner-profile/postbacks/[postbackId]
export const PATCH = withPartnerProfile(
async ({ partner, params, req }) => {
const { postbackId } = params;

let postback = await getPostbackOrThrow({
postbackId,
partnerId: partner.id,
});

const { name, url, triggers, disabled } = updatePostbackInputSchema.parse(
await parseRequestBody(req),
);

postback = await prisma.postback.update({
where: {
id: postbackId,
},
data: {
...(name !== undefined && { name }),
...(url !== undefined && { url }),
...(triggers !== undefined && { triggers }),
...(disabled !== undefined && {
disabledAt: disabled ? new Date() : null,
}),
},
});

return NextResponse.json(postbackSchema.parse(postback));
},
{
requiredPermission: "postbacks.write",
featureFlag: "postbacks",
},
);

// DELETE /api/partner-profile/postbacks/[postbackId]
export const DELETE = withPartnerProfile(
async ({ partner, params }) => {
const { postbackId } = params;

await getPostbackOrThrow({
postbackId,
partnerId: partner.id,
});

await prisma.postback.delete({
where: {
id: postbackId,
},
});

return NextResponse.json({ id: postbackId });
},
{
requiredPermission: "postbacks.write",
featureFlag: "postbacks",
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { DubApiError } from "@/lib/api/errors";
import { getPostbackOrThrow } from "@/lib/api/postbacks/get-postback-or-throw";
import { parseRequestBody } from "@/lib/api/utils";
import { withPartnerProfile } from "@/lib/auth/partner";
import { sendPartnerPostback } from "@/lib/postback/api/send-partner-postback";
import commissionCreated from "@/lib/postback/sample-events/commission-created.json";
import leadCreated from "@/lib/postback/sample-events/lead-created.json";
import saleCreated from "@/lib/postback/sample-events/sale-created.json";
import { sendTestPostbackInputSchema } from "@/lib/postback/schemas";
import { PostbackTrigger } from "@/lib/types";
import { NextResponse } from "next/server";

const samplePayloads: Record<PostbackTrigger, Record<string, unknown>> = {
"lead.created": leadCreated,
"sale.created": saleCreated,
"commission.created": commissionCreated,
};

// POST /api/partner-profile/postbacks/[postbackId]/send-test
export const POST = withPartnerProfile(
async ({ partner, params, req }) => {
const { postbackId } = params;

const { event } = sendTestPostbackInputSchema.parse(
await parseRequestBody(req),
);

const postback = await getPostbackOrThrow({
postbackId,
partnerId: partner.id,
});

const triggers = postback.triggers as string[];

if (!triggers.includes(event)) {
throw new DubApiError({
code: "bad_request",
message: "The selected event is not configured for this postback.",
});
}

await sendPartnerPostback({
partnerId: partner.id,
event,
data: samplePayloads[event],
skipEnrichment: true,
});

return NextResponse.json({});
},
{
requiredPermission: "postbacks.write",
featureFlag: "postbacks",
},
);
86 changes: 86 additions & 0 deletions apps/web/app/(ee)/api/partner-profile/postbacks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { createId } from "@/lib/api/create-id";
import { DubApiError } from "@/lib/api/errors";
import { createToken } from "@/lib/api/oauth/utils";
import { parseRequestBody } from "@/lib/api/utils";
import { withPartnerProfile } from "@/lib/auth/partner";
import { identifyPostbackChannel } from "@/lib/postback/api/utils";
import {
MAX_POSTBACKS,
POSTBACK_SECRET_LENGTH,
POSTBACK_SECRET_PREFIX,
} from "@/lib/postback/constants";
import {
createPostbackInputSchema,
createPostbackOutputSchema,
postbackSchema,
} from "@/lib/postback/schemas";
import { prisma } from "@dub/prisma";
import { NextResponse } from "next/server";
import * as z from "zod/v4";

// GET /api/partner-profile/postbacks
export const GET = withPartnerProfile(
async ({ partner }) => {
const postbacks = await prisma.postback.findMany({
where: {
partnerId: partner.id,
},
orderBy: {
createdAt: "desc",
},
});

return NextResponse.json(z.array(postbackSchema).parse(postbacks));
},
{
requiredPermission: "postbacks.read",
featureFlag: "postbacks",
},
);

// POST /api/partner-profile/postbacks
export const POST = withPartnerProfile(
async ({ partner, req }) => {
const { name, url, triggers } = createPostbackInputSchema.parse(
await parseRequestBody(req),
);

const postbackCount = await prisma.postback.count({
where: {
partnerId: partner.id,
},
});

if (postbackCount >= MAX_POSTBACKS) {
throw new DubApiError({
code: "exceeded_limit",
message: `Maximum number of postbacks (${MAX_POSTBACKS}) reached.`,
});
}

const secret = createToken({
prefix: POSTBACK_SECRET_PREFIX,
length: POSTBACK_SECRET_LENGTH,
});

const postback = await prisma.postback.create({
data: {
id: createId({ prefix: "pb_" }),
partnerId: partner.id,
name,
url,
secret,
triggers,
receiver: identifyPostbackChannel(url),
},
});

return NextResponse.json(createPostbackOutputSchema.parse(postback), {
status: 201,
});
},
{
requiredPermission: "postbacks.write",
featureFlag: "postbacks",
},
);
4 changes: 4 additions & 0 deletions apps/web/app/(ee)/api/partner-profile/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { withPartnerProfile } from "@/lib/auth/partner";
import { getPartnerFeatureFlags } from "@/lib/edge-config";
import { NextResponse } from "next/server";

// GET /api/partner-profile - get a partner profile
export const GET = withPartnerProfile(async ({ partner, partnerUser }) => {
const featureFlags = await getPartnerFeatureFlags(partner.id);

return NextResponse.json({
...partnerUser,
...partner,
featureFlags,
});
});
2 changes: 1 addition & 1 deletion apps/web/app/(ee)/api/partners/analytics/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { getAnalytics } from "@/lib/analytics/get-analytics";
import { getStartEndDates } from "@/lib/analytics/utils/get-start-end-dates";
import { parseFilterValue } from "@dub/utils";
import { DubApiError } from "@/lib/api/errors";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { withWorkspace } from "@/lib/auth";
Expand All @@ -11,6 +10,7 @@ import {
partnersTopLinksSchema,
} from "@/lib/zod/schemas/partners";
import { prisma } from "@dub/prisma";
import { parseFilterValue } from "@dub/utils";
import { format } from "date-fns";
import { NextResponse } from "next/server";

Expand Down
Loading