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
4 changes: 3 additions & 1 deletion apps/backend/src/app/api/latest/emails/send-email/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export const POST = createSmartRouteHandler({
}).defined(),
body: yupObject({
user_ids: yupArray(yupString().defined()).defined(),
theme_id: templateThemeIdSchema.nullable().label("The theme to use for the email. If not specified, the default theme will be used."),
theme_id: templateThemeIdSchema.nullable().meta({
openapiField: { description: "The theme to use for the email. If not specified, the default theme will be used." }
}),
html: yupString().optional(),
subject: yupString().optional(),
notification_category_name: yupString().optional(),
Expand Down
9 changes: 1 addition & 8 deletions apps/backend/src/lib/email-rendering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,6 @@ export async function renderEmailWithTemplate(
throw new StackAssertionError("Project is required when not in preview mode", { user, project, variables });
}

if (["development", "test"].includes(getNodeEnvironment()) && apiKey === "mock_stack_freestyle_key") {
return Result.ok({
html: `<div>Mock api key detected, \n\ntemplateComponent: ${templateComponent}\n\nthemeComponent: ${themeComponent}\n\n variables: ${JSON.stringify(variables)}</div>`,
text: `<div>Mock api key detected, \n\ntemplateComponent: ${templateComponent}\n\nthemeComponent: ${themeComponent}\n\n variables: ${JSON.stringify(variables)}</div>`,
subject: `Mock subject, ${templateComponent.match(/<Subject\s+[^>]*\/>/g)?.[0]}`,
notificationCategory: "mock notification category",
});
}
const result = await bundleJavaScript({
"/utils.tsx": findComponentValueUtil,
"/theme.tsx": themeComponent,
Expand Down Expand Up @@ -124,6 +116,7 @@ export async function renderEmailWithTemplate(

const freestyle = new Freestyle({ apiKey });
const nodeModules = {
"react": "19.1.1",
"@react-email/components": "0.1.1",
"arktype": "2.1.20",
};
Expand Down
12 changes: 10 additions & 2 deletions apps/backend/src/lib/freestyle.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { traceSpan } from '@/utils/telemetry';
import { getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env';
import { StackAssertionError, captureError, errorToNiceString } from '@stackframe/stack-shared/dist/utils/errors';
import { traceSpan } from '@stackframe/stack-shared/dist/utils/telemetry';
import { FreestyleSandboxes } from 'freestyle-sandboxes';

export class Freestyle {
private freestyle: FreestyleSandboxes;

constructor(options: { apiKey: string }) {
this.freestyle = new FreestyleSandboxes(options);
let baseUrl = undefined;
if (["development", "test"].includes(getNodeEnvironment()) && options.apiKey === "mock_stack_freestyle_key") {
baseUrl = "http://localhost:8122";
}
this.freestyle = new FreestyleSandboxes({
apiKey: options.apiKey,
baseUrl,
});
}

async executeScript(script: string, options?: Parameters<FreestyleSandboxes['executeScript']>[1]) {
Expand Down
2 changes: 1 addition & 1 deletion apps/dev-launchpad/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ <h2 style="margin-top: 64px;">Background services</h2>
4318: OTel collector
</li>
<li>
8119: Freestyle mock
8122: Freestyle mock
</li>
<li>
8121: S3 mock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ it("should send a sign-in code per e-mail", async ({ expect }) => {
[
MailboxMessage {
"from": "Stack Dashboard <noreply@example.com>",
"subject": "Mock subject, <Subject value=\\"{\\"Sign in to \\" + project.displayName + \\": Your code is \\" + variables.otp} />\\"",
"subject": "Sign in to Stack Dashboard: Your code is <stripped code>",
"to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"],
<some fields may have been hidden>,
},
Expand Down Expand Up @@ -100,31 +100,12 @@ it("should send otp code to user", async ({ expect }) => {
});

const email = (await backendContext.value.mailbox.fetchMessages()).findLast((email) => email.subject.includes("Sign in"));
const match = email?.body?.text.match(/"otp":"([A-Z0-9]{6})"/);
const match = email?.body?.html.match(/\>([A-Z0-9]{6})\<\/p\>/);
expect(match).toHaveLength(2);
const code = match?.[1];
expect(code).toHaveLength(6);
});

it("should not send otp code to user if client version is older equal to 2.5.37", async ({ expect }) => {
await Auth.Otp.sendSignInCode();
const mailbox = backendContext.value.mailbox;
await niceBackendFetch("/api/v1/auth/otp/send-sign-in-code", {
method: "POST",
accessType: "client",
body: {
email: mailbox.emailAddress,
callback_url: "http://localhost:12345/some-callback-url",
},
headers: {
"X-Stack-Client-Version": "js @stackframe/stack@2.5.37",
},
});

const email = (await backendContext.value.mailbox.fetchMessages()).findLast((email) => email.subject.includes("Sign in"));
const match = email?.body?.text.match(/^[A-Z0-9]{6}$/sm);
expect(match).toBeNull();
});

it.todo("should create a team for newly created users if configured as such");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ it("should sign in with otp code", async ({ expect }) => {
expect(sendSignInCodeResponse.body.nonce).toBeDefined();

const email = (await backendContext.value.mailbox.fetchMessages()).findLast((message) => message.subject.includes("Sign in to")) ?? throwErr("Sign-in code message not found");
const match = email.body?.text.match(/"otp":"([A-Z0-9]{6})"/);
const match = email.body?.html.match(/\>([A-Z0-9]{6})\<\/p\>/);

const signInResponse = await niceBackendFetch("/api/v1/auth/otp/sign-in", {
method: "POST",
Expand Down Expand Up @@ -270,7 +270,7 @@ it("should set the code to invalid after too many attempts", async ({ expect })
});

const email = (await backendContext.value.mailbox.fetchMessages()).findLast((message) => message.subject.includes("Sign in to")) ?? throwErr("Sign-in code message not found");
const match = email.body?.text.match(/"otp":"([A-Z0-9]{6})"/);
const match = email.body?.html.match(/\>([A-Z0-9]{6})\<\/p\>/);

for (let i = 0; i < 25; i++) {
await niceBackendFetch("/api/v1/auth/otp/sign-in", {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ async function getResetCode() {
`);
const messages = await backendContext.value.mailbox.fetchMessages();
const messagesNoBody = await backendContext.value.mailbox.fetchMessages({ noBody: true });
expect(messagesNoBody.at(-1)).toMatchInlineSnapshot(`
MailboxMessage {
expect(messagesNoBody.find(m => m.subject.includes("Reset your password at"))).toMatchInlineSnapshot(`
MailboxMessage {
"from": "Stack Dashboard <noreply@example.com>",
"subject": "Mock subject, <Subject value=\\"{\\"Reset your password at \\" + project.displayName} />\\"",
"subject": "Reset your password at Stack Dashboard",
"to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"],
<some fields may have been hidden>,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ it("should send a password reset code per e-mail", async ({ expect }) => {
[
MailboxMessage {
"from": "Stack Dashboard <noreply@example.com>",
"subject": "Mock subject, <Subject value={\`Verify your email at \${project.displayName}\`} />",
"subject": "Verify your email at Stack Dashboard",
"to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"],
<some fields may have been hidden>,
},
MailboxMessage {
"from": "Stack Dashboard <noreply@example.com>",
"subject": "Mock subject, <Subject value=\\"{\\"Reset your password at \\" + project.displayName} />\\"",
"subject": "Reset your password at Stack Dashboard",
"to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"],
<some fields may have been hidden>,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ it("should sign up new users", async ({ expect }) => {
[
MailboxMessage {
"from": "Stack Dashboard <noreply@example.com>",
"subject": "Mock subject, <Subject value={\`Verify your email at \${project.displayName}\`} />",
"subject": "Verify your email at Stack Dashboard",
"to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"],
<some fields may have been hidden>,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@
callback_url: "http://localhost:12345/some-callback-url",
},
});
expect(await backendContext.value.mailbox.fetchMessages({ noBody: true })).toMatchInlineSnapshot(`

Check failure on line 51 in apps/e2e/tests/backend/endpoints/api/v1/contact-channels/legacy-send-verification-code.test.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

tests/backend/endpoints/api/v1/contact-channels/legacy-send-verification-code.test.ts > should send a verification code per e-mail

Error: Snapshot `should send a verification code per e-mail 1` mismatched - Expected + Received @@ -3,12 +3,6 @@ "from": "Stack Dashboard <noreply@example.com>", "subject": "Verify your email at Stack Dashboard", "to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"], <some fields may have been hidden>, }, - MailboxMessage { - "from": "Stack Dashboard <noreply@example.com>", - "subject": "Verify your email at Stack Dashboard", - "to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"], - <some fields may have been hidden>, - }, ] ❯ tests/backend/endpoints/api/v1/contact-channels/legacy-send-verification-code.test.ts:51:78
[
MailboxMessage {
"from": "Stack Dashboard <noreply@example.com>",
"subject": "Mock subject, <Subject value={\`Verify your email at \${project.displayName}\`} />",
"subject": "Verify your email at Stack Dashboard",
"to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"],
<some fields may have been hidden>,
},
MailboxMessage {
"from": "Stack Dashboard <noreply@example.com>",
"subject": "Mock subject, <Subject value={\`Verify your email at \${project.displayName}\`} />",
"subject": "Verify your email at Stack Dashboard",
"to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"],
<some fields may have been hidden>,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ it("should send a verification code per e-mail", async ({ expect }) => {
[
MailboxMessage {
"from": "Stack Dashboard <noreply@example.com>",
"subject": "Mock subject, <Subject value={\`Verify your email at \${project.displayName}\`} />",
"subject": "Verify your email at Stack Dashboard",
"to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"],
<some fields may have been hidden>,
},
MailboxMessage {
"from": "Stack Dashboard <noreply@example.com>",
"subject": "Mock subject, <Subject value={\`Verify your email at \${project.displayName}\`} />",
"subject": "Verify your email at Stack Dashboard",
"to": ["<default-mailbox--<stripped UUID>@stack-generated.example.com>"],
<some fields may have been hidden>,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ it("each verification code that was already requested can be used exactly once",
await ContactChannels.sendVerificationCode();
const mailbox = backendContext.value.mailbox;
const messages = await mailbox.fetchMessages();
const verifyMessages = messages.filter((message) => message.subject === "Mock subject, <Subject value={\`Verify your email at \${project.displayName}\`} />");
const verifyMessages = messages.filter((message) => message.subject === "Verify your email at Stack Dashboard");
const verificationCodes = verifyMessages.map((message) => message.body?.text.match(/http:\/\/localhost:12345\/some-callback-url\?code=([a-zA-Z0-9]+)/)?.[1] ?? throwErr("Verification code not found"));
expect(verificationCodes).toHaveLength(3);

Expand Down
17 changes: 14 additions & 3 deletions apps/e2e/tests/backend/endpoints/api/v1/email-themes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ describe("get email theme", () => {
"body": {
"display_name": "Default Light",
"tsx_source": deindent\`
import { Html, Head, Tailwind, Body, Container } from '@react-email/components';
import { Html, Head, Tailwind, Body, Container, Link } from '@react-email/components';
import { ThemeProps } from "@stackframe/emails"

export function EmailTheme({ children }: { children: React.ReactNode }) {
export function EmailTheme({ children, unsubscribeLink }: ThemeProps) {
return (
<Html>
<Head />
Expand All @@ -102,12 +103,22 @@ describe("get email theme", () => {
<Container className="bg-white p-[45px] rounded-lg">
{children}
</Container>
{unsubscribeLink && (
<div className="p-4">
<Link href={unsubscribeLink}>Click here{" "}</Link>
to unsubscribe from these emails
</div>
)}
</Body>
</Tailwind>
</Html>
);
}
\`,

EmailTheme.PreviewProps = {
unsubscribeLink: "https://example.com"
} satisfies Partial<ThemeProps>
\` + "\\n",
},
"headers": Headers { <some fields may have been hidden> },
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@ it("should not allow updating email templates when using shared email config", a
'x-stack-admin-access-token': adminAccessToken,
},
body: {
tsx_source: "mock_tsx_source",
tsx_source: `
import { Subject, NotificationCategory } from '@stackframe/emails';
export const variablesSchema = (v) => v;
export function EmailTemplate() {
return <>
<Subject value="Test Subject" />
<NotificationCategory value="Transactional" />
<div>Mock email template</div>
</>;
}
`,
},
});

Expand Down Expand Up @@ -56,39 +66,24 @@ it("should allow adding and updating email templates with custom email config",
method: "PATCH",
accessType: "admin",
body: {
tsx_source: "mock_tsx_source",
tsx_source: `
import { Subject, NotificationCategory } from '@stackframe/emails';
export const variablesSchema = (v) => v;
export function EmailTemplate() {
return <>
<Subject value="Test Subject" />
<NotificationCategory value="Transactional" />
<div>Mock email template</div>
</>;
}
`,
},
});

expect(updateResponse).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"rendered_html": deindent\`
<div>Mock api key detected,

templateComponent: mock_tsx_source

themeComponent: import { Html, Head, Tailwind, Body, Container } from '@react-email/components';

export function EmailTheme({ children }: { children: React.ReactNode }) {
return (
<Html>
<Head />
<Tailwind>
<Body className="bg-[#fafbfb] font-sans text-base">
<Container className="bg-white p-[45px] rounded-lg">
{children}
</Container>
</Body>
</Tailwind>
</Html>
);
}

variables: {"projectDisplayName":"New Project"}</div>
\`,
},
"body": { "rendered_html": "<!DOCTYPE html PUBLIC \\"-//W3C//DTD XHTML 1.0 Transitional//EN\\" \\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\\"><html dir=\\"ltr\\" lang=\\"en\\"><head><meta content=\\"text/html; charset=UTF-8\\" http-equiv=\\"Content-Type\\"/><meta name=\\"x-apple-disable-message-reformatting\\"/></head><body style=\\"background-color:rgb(250,251,251);font-family:ui-sans-serif, system-ui, sans-serif, &quot;Apple Color Emoji&quot;, &quot;Segoe UI Emoji&quot;, &quot;Segoe UI Symbol&quot;, &quot;Noto Color Emoji&quot;;font-size:1rem;line-height:1.5rem\\"><!--$--><table align=\\"center\\" width=\\"100%\\" border=\\"0\\" cellPadding=\\"0\\" cellSpacing=\\"0\\" role=\\"presentation\\" style=\\"background-color:rgb(255,255,255);padding:45px;border-radius:0.5rem;max-width:37.5em\\"><tbody><tr style=\\"width:100%\\"><td><div>Mock email template</div></td></tr></tbody></table><div style=\\"padding:1rem\\"><a href=\\"https://example.com\\" style=\\"color:#067df7;text-decoration-line:none\\" target=\\"_blank\\">Click here<!-- --> </a>to unsubscribe from these emails</div><!--7--><!--/$--></body></html>" },
"headers": Headers { <some fields may have been hidden> },
}
`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ it("should list sent emails for the current project", async ({ expect }) => {
"username": "does not matter, ignored by Inbucket",
},
"sent_at_millis": <stripped field 'sent_at_millis'>,
"subject": "Mock subject, <Subject value={\\"Sign in to \\" + project.displayName + \\": Your code is \\" + variables.otp} />",
"subject": "Sign in to New Project: Your code is <stripped code>",
"to": ["default-mailbox--<stripped UUID>@stack-generated.example.com"],
},
{
Expand All @@ -55,7 +55,7 @@ it("should list sent emails for the current project", async ({ expect }) => {
"username": "does not matter, ignored by Inbucket",
},
"sent_at_millis": <stripped field 'sent_at_millis'>,
"subject": "Mock subject, <Subject value={\\"Sign in to \\" + project.displayName + \\": Your code is \\" + variables.otp} />",
"subject": "Sign in to New Project: Your code is <stripped code>",
"to": ["default-mailbox--<stripped UUID>@stack-generated.example.com"],
},
],
Expand Down Expand Up @@ -102,7 +102,7 @@ it("should not allow two different projects to see the same send log", async ({
"username": "does not matter, ignored by Inbucket",
},
"sent_at_millis": <stripped field 'sent_at_millis'>,
"subject": "Mock subject, <Subject value={\\"Sign in to \\" + project.displayName + \\": Your code is \\" + variables.otp} />",
"subject": "Sign in to New Project: Your code is <stripped code>",
"to": ["default-mailbox--<stripped UUID>@stack-generated.example.com"],
},
],
Expand Down
Loading
Loading