Thanks to visit codestin.com
Credit goes to github.com

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

Large diffs are not rendered by default.

65 changes: 43 additions & 22 deletions apps/web/lib/actions/partners/invite-partner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,30 +69,51 @@ export const invitePartnerAction = authActionClient
status: "invited",
});

waitUntil(
Promise.allSettled([
(async () => {
await sendEmail({
subject: `${program.name} invited you to join Dub Partners`,
variant: "notifications",
to: email,
replyTo: program.supportEmail || "noreply",
react: ProgramInvite({
email,
name: enrolledPartner.name,
program: {
name: program.name,
slug: program.slug,
logo: program.logo,
},
...(await getPartnerInviteRewardsAndBounties({
programId,
groupId: enrolledPartner.groupId || program.defaultGroupId,
})),
// Use saved invite email data from program if available
const inviteEmailData = program.inviteEmailData;

const sendPartnerInvitePromise = (async () => {
try {
const rewardsAndBounties = await getPartnerInviteRewardsAndBounties({
programId,
groupId: enrolledPartner.groupId || program.defaultGroupId,
});

await sendEmail({
subject:
inviteEmailData?.subject ||
`${program.name} invited you to join Dub Partners`,
variant: "notifications",
to: email,
replyTo: program.supportEmail || "noreply",
react: ProgramInvite({
email,
name: enrolledPartner.name,
program: {
name: program.name,
slug: program.slug,
logo: program.logo,
},
...(inviteEmailData?.subject && {
subject: inviteEmailData.subject,
}),
});
})(),
...(inviteEmailData?.title && { title: inviteEmailData.title }),
...(inviteEmailData?.body && { body: inviteEmailData.body }),
...rewardsAndBounties,
}),
});
} catch (error) {
console.error("Failed to send partner invite email", {
error,
partnerId: enrolledPartner.partnerId || enrolledPartner.id,
programId,
});
}
})();

waitUntil(
Promise.allSettled([
sendPartnerInvitePromise,
recordAuditLog({
workspaceId: workspace.id,
programId,
Expand Down
45 changes: 45 additions & 0 deletions apps/web/lib/actions/partners/save-invite-email-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use server";

import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { sanitizeMarkdown } from "@/lib/partners/sanitize-markdown";
import { prisma } from "@dub/prisma";
import { z } from "zod";
import { authActionClient } from "../safe-action";

const saveInviteEmailDataSchema = z.object({
workspaceId: z.string(),
subject: z.string().trim().min(1),
title: z.string().trim().min(1),
body: z.string().trim().min(1).max(3000),
});

export const saveInviteEmailDataAction = authActionClient
.schema(saveInviteEmailDataSchema)
.action(async ({ parsedInput, ctx }) => {
const { workspace } = ctx;
const { subject, title, body } = parsedInput;

const programId = getDefaultProgramIdOrThrow(workspace);

// Sanitize emailBody before saving
const sanitizedBody = sanitizeMarkdown(body);

if (!sanitizedBody) {
throw new Error(
"Email body contains invalid content. Please remove excessively long lines or unsupported characters.",
);
}

await prisma.program.update({
where: {
id: programId,
},
data: {
inviteEmailData: {
subject: subject.trim(),
title: title.trim(),
body: sanitizedBody,
},
},
});
});
5 changes: 4 additions & 1 deletion apps/web/lib/api/programs/get-program-or-throw.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { programInviteEmailDataSchema } from "@/lib/zod/schemas/program-invite-email";
import { ProgramSchema } from "@/lib/zod/schemas/programs";
import { prisma } from "@dub/prisma";
import { DubApiError } from "../errors";
Expand All @@ -22,5 +23,7 @@ export const getProgramOrThrow = async ({
});
}

return ProgramSchema.parse(program);
return ProgramSchema.extend({
inviteEmailData: programInviteEmailDataSchema,
}).parse(program);
};
51 changes: 51 additions & 0 deletions apps/web/lib/partners/sanitize-markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"server-only";

/**
* Sanitizes and validates markdown content for safe use in email templates.
*
* This function:
* - Trims whitespace
* - Validates the content is valid text (not binary)
* - Checks for suspicious patterns that could cause DoS issues
* - Normalizes line endings
*
* @param markdown - The markdown string to sanitize
* @returns The sanitized markdown string, or null if invalid/binary content detected
*/
export function sanitizeMarkdown(
markdown: string | null | undefined,
): string | null {
if (!markdown || typeof markdown !== "string") {
return null;
}

// Trim whitespace
let sanitized = markdown.trim();

// Return null if empty after trimming
if (!sanitized) {
return null;
}

// Check for binary content - markdown should be valid UTF-8 text
// Reject if there are null bytes (indicates binary content)
if (sanitized.includes("\0")) {
return null;
}

// Check for suspicious patterns that could cause DoS or rendering issues
// Reject content with excessively long lines to avoid malformed markdown
const maxLineLength = 1000;
const hasExcessivelyLongLine = sanitized
.split("\n")
.some((line) => line.length > maxLineLength);

if (hasExcessivelyLongLine) {
return null;
}

// Normalize line endings
sanitized = sanitized.replace(/\r\n/g, "\n").replace(/\r/g, "\n");

return sanitized;
}
5 changes: 5 additions & 0 deletions apps/web/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import {
programApplicationFormFieldWithValuesSchema,
programApplicationFormSchema,
} from "./zod/schemas/program-application-form";
import { programInviteEmailDataSchema } from "./zod/schemas/program-invite-email";
import { programLanderSchema } from "./zod/schemas/program-lander";
import { programDataSchema } from "./zod/schemas/program-onboarding";
import {
Expand Down Expand Up @@ -465,6 +466,10 @@ export type DiscountCodeProps = z.infer<typeof DiscountCodeSchema>;

export type ProgramProps = z.infer<typeof ProgramSchema>;

export type ProgramInviteEmailData = z.infer<
typeof programInviteEmailDataSchema
>;

export type ProgramLanderData = z.infer<typeof programLanderSchema>;

export type ProgramApplicationFormData = z.infer<
Expand Down
3 changes: 3 additions & 0 deletions apps/web/lib/zod/schemas/partners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,9 @@ export const invitePartnerSchema = z.object({
email: z.string().trim().email().min(1).max(100),
username: z.string().max(100).optional(),
groupId: z.string().nullish().default(null),
emailSubject: z.string().optional(),
emailTitle: z.string().optional(),
emailBody: z.string().max(3000).optional(),
});

export const approvePartnerSchema = z.object({
Expand Down
9 changes: 9 additions & 0 deletions apps/web/lib/zod/schemas/program-invite-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from "zod";

export const programInviteEmailDataSchema = z
.object({
subject: z.string(),
title: z.string(),
body: z.string(),
})
.nullish();
1 change: 1 addition & 0 deletions packages/email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"dependencies": {
"@dub/utils": "workspace:*",
"@react-email/components": "^0.0.22",
"@react-email/markdown": "^0.0.17",
"lucide-react": "^0.462.0",
"nodemailer": "^6.9.3",
"react-email": "^2.1.6",
Expand Down
Loading