mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
creating templates and variableSchemas (#808)
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Docker Emulator Test / docker (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Test / docker (push) Has been cancelled
Runs E2E API Tests / build (22.x) (push) Has been cancelled
Runs E2E API Tests with external source of truth / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
Docker Emulator Test / docker (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Test / docker (push) Has been cancelled
Runs E2E API Tests / build (22.x) (push) Has been cancelled
Runs E2E API Tests with external source of truth / build (22.x) (push) Has been cancelled
Lint & build / lint_and_build (latest) (push) Has been cancelled
Dev Environment Test / restart-dev-and-test (push) Has been cancelled
Run setup tests / setup-tests (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
This commit is contained in:
parent
0a72170b63
commit
2334884a79
@ -1,7 +1,7 @@
|
||||
import { renderEmailWithTemplate } from "@/lib/email-rendering";
|
||||
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
|
||||
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
|
||||
import { adaptSchema, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { adaptSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { captureError, StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
|
||||
|
||||
@ -28,7 +28,6 @@ export const POST = createSmartRouteHandler({
|
||||
bodyType: yupString().oneOf(["json"]).defined(),
|
||||
body: yupObject({
|
||||
html: yupString().defined(),
|
||||
schema: yupMixed(),
|
||||
subject: yupString(),
|
||||
notification_category: yupString(),
|
||||
}).defined(),
|
||||
@ -50,18 +49,14 @@ export const POST = createSmartRouteHandler({
|
||||
if (!templateSource) {
|
||||
throw new StatusError(400, "No template found with given id");
|
||||
}
|
||||
const variables = {
|
||||
projectDisplayName: tenancy.project.display_name,
|
||||
teamDisplayName: "My Team",
|
||||
userDisplayName: "John Doe",
|
||||
emailVerificationLink: "<email verification link>",
|
||||
otp: "3SLSWZ",
|
||||
magicLink: "<magic link>",
|
||||
passwordResetLink: "<password reset link>",
|
||||
teamInvitationLink: "<team invitation link>",
|
||||
signInInvitationLink: "<sign in invitation link>",
|
||||
};
|
||||
const result = await renderEmailWithTemplate(templateSource, themeSource, variables);
|
||||
const result = await renderEmailWithTemplate(
|
||||
templateSource,
|
||||
themeSource,
|
||||
{
|
||||
project: { displayName: tenancy.project.display_name },
|
||||
previewMode: true,
|
||||
},
|
||||
);
|
||||
if ("error" in result) {
|
||||
captureError('render-email', new StackAssertionError("Error rendering email with theme", { result }));
|
||||
throw new KnownErrors.EmailRenderingError(result.error);
|
||||
@ -71,7 +66,6 @@ export const POST = createSmartRouteHandler({
|
||||
bodyType: "json",
|
||||
body: {
|
||||
html: result.data.html,
|
||||
schema: result.data.schema,
|
||||
subject: result.data.subject,
|
||||
notification_category: result.data.notificationCategory,
|
||||
},
|
||||
|
||||
@ -116,7 +116,14 @@ export const POST = createSmartRouteHandler({
|
||||
|
||||
|
||||
const template = createTemplateComponentFromHtml(body.html, unsubscribeLink || undefined);
|
||||
const renderedEmail = await renderEmailWithTemplate(template, activeTheme.tsxSource);
|
||||
const renderedEmail = await renderEmailWithTemplate(
|
||||
template,
|
||||
activeTheme.tsxSource,
|
||||
{
|
||||
user: { displayName: user.displayName },
|
||||
project: { displayName: auth.tenancy.project.display_name },
|
||||
},
|
||||
);
|
||||
if (renderedEmail.status === "error") {
|
||||
userSendErrors.set(userId, "There was an error rendering the email");
|
||||
continue;
|
||||
|
||||
@ -37,7 +37,10 @@ export const PATCH = createSmartRouteHandler({
|
||||
throw new StatusError(StatusError.NotFound, "No template found with given id");
|
||||
}
|
||||
const theme = tenancy.completeConfig.emails.themes[tenancy.completeConfig.emails.selectedThemeId];
|
||||
const result = await renderEmailWithTemplate(body.tsx_source, theme.tsxSource, { projectDisplayName: tenancy.project.display_name });
|
||||
const result = await renderEmailWithTemplate(body.tsx_source, theme.tsxSource, {
|
||||
variables: { projectDisplayName: tenancy.project.display_name },
|
||||
previewMode: true,
|
||||
});
|
||||
if (result.status === "error") {
|
||||
throw new KnownErrors.EmailRenderingError(result.error);
|
||||
}
|
||||
@ -47,9 +50,6 @@ export const PATCH = createSmartRouteHandler({
|
||||
if (result.data.notificationCategory === undefined) {
|
||||
throw new KnownErrors.EmailRenderingError("NotificationCategory is required, import it from @stackframe/emails");
|
||||
}
|
||||
if (result.data.schema === undefined) {
|
||||
throw new KnownErrors.EmailRenderingError("schema is required and must be exported");
|
||||
}
|
||||
|
||||
await overrideEnvironmentConfigOverride({
|
||||
tx: globalPrismaClient,
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
|
||||
import { adaptSchema, yupArray, yupNumber, yupObject, yupString, templateThemeIdSchema } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
|
||||
import { adaptSchema, templateThemeIdSchema, yupArray, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
|
||||
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
import { overrideEnvironmentConfigOverride } from "@/lib/config";
|
||||
import { globalPrismaClient } from "@/prisma-client";
|
||||
|
||||
export const GET = createSmartRouteHandler({
|
||||
metadata: {
|
||||
@ -40,3 +43,71 @@ export const GET = createSmartRouteHandler({
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const POST = createSmartRouteHandler({
|
||||
metadata: {
|
||||
hidden: true,
|
||||
},
|
||||
request: yupObject({
|
||||
auth: yupObject({
|
||||
type: yupString().oneOf(["admin"]).defined(),
|
||||
tenancy: adaptSchema.defined(),
|
||||
}).defined(),
|
||||
body: yupObject({
|
||||
display_name: yupString().defined(),
|
||||
}),
|
||||
}),
|
||||
response: yupObject({
|
||||
statusCode: yupNumber().oneOf([200]).defined(),
|
||||
bodyType: yupString().oneOf(["json"]).defined(),
|
||||
body: yupObject({
|
||||
id: yupString().defined(),
|
||||
}).defined(),
|
||||
}),
|
||||
async handler({ body, auth: { tenancy } }) {
|
||||
const id = generateUuid();
|
||||
const defaultTemplateSource = deindent`
|
||||
import { type } from "arktype"
|
||||
import { Container } from "@react-email/components";
|
||||
import { Subject, NotificationCategory, Props } from "@stackframe/emails";
|
||||
|
||||
export const variablesSchema = type({
|
||||
count: "number"
|
||||
});
|
||||
|
||||
export function EmailTemplate({ user, variables }: Props<typeof variablesSchema.infer>) {
|
||||
return (
|
||||
<Container>
|
||||
<Subject value={\`Hello \${user.displayName}!\`} />
|
||||
<NotificationCategory value="Transactional" />
|
||||
<div className="font-bold">Hi {user.displayName}!</div>
|
||||
<br />
|
||||
count is {variables.count}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
EmailTemplate.PreviewVariables = {
|
||||
count: 10
|
||||
} satisfies typeof variablesSchema.infer
|
||||
`;
|
||||
|
||||
await overrideEnvironmentConfigOverride({
|
||||
tx: globalPrismaClient,
|
||||
projectId: tenancy.project.id,
|
||||
branchId: tenancy.branchId,
|
||||
environmentConfigOverrideOverride: {
|
||||
[`emails.templates.${id}`]: {
|
||||
displayName: body.display_name,
|
||||
tsxSource: defaultTemplateSource,
|
||||
themeId: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
return {
|
||||
statusCode: 200,
|
||||
bodyType: "json",
|
||||
body: { id },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@ -123,15 +123,9 @@ export const POST = createSmartRouteHandler({
|
||||
emailTemplates[configTemplateId].tsxSource,
|
||||
emptyThemeComponent,
|
||||
{
|
||||
projectDisplayName: "My Project",
|
||||
teamDisplayName: "My Team",
|
||||
userDisplayName: "John Doe",
|
||||
emailVerificationLink: "<email verification link>",
|
||||
otp: "3SLSWZ",
|
||||
magicLink: "<magic link>",
|
||||
passwordResetLink: "<password reset link>",
|
||||
teamInvitationLink: "<team invitation link>",
|
||||
signInInvitationLink: "<sign in invitation link>",
|
||||
project: { displayName: project.displayName },
|
||||
user: { displayName: "John Doe" },
|
||||
previewMode: true,
|
||||
}
|
||||
);
|
||||
rendered.push({
|
||||
|
||||
@ -74,7 +74,11 @@ export const PATCH = createSmartRouteHandler({
|
||||
throw new StatusError(404, "No theme found with given id");
|
||||
}
|
||||
const theme = themeList[id];
|
||||
const result = await renderEmailWithTemplate(previewTemplateSource, body.tsx_source);
|
||||
const result = await renderEmailWithTemplate(
|
||||
previewTemplateSource,
|
||||
body.tsx_source,
|
||||
{ previewMode: true },
|
||||
);
|
||||
if (result.status === "error") {
|
||||
throw new KnownErrors.EmailRenderingError(result.error);
|
||||
}
|
||||
|
||||
@ -28,30 +28,36 @@ Create a new email template.
|
||||
The email template is a tsx file that is used to render the email content.
|
||||
It must use react-email components.
|
||||
It must export two things:
|
||||
- schema: An arktype schema for the email template props
|
||||
- EmailTemplate: A function that renders the email template.
|
||||
- variablesSchema: An arktype schema for the email template props
|
||||
- EmailTemplate: A function that renders the email template. You must set the PreviewVariables property to an object that satisfies the variablesSchema by doing EmailTemplate.PreviewVariables = { ...
|
||||
It must not import from any package besides "@react-email/components", "@stackframe/emails", and "arktype".
|
||||
It uses tailwind classes for all styling.
|
||||
|
||||
Here is an example of a valid email template:
|
||||
\`\`\`tsx
|
||||
import { type } from "arktype"
|
||||
import { Container } from "@react-email/components";
|
||||
import { Subject, NotificationCategory } from "@stackframe/emails";
|
||||
import { type } from "arktype";
|
||||
import { Subject, NotificationCategory, Props } from "@stackframe/emails";
|
||||
|
||||
export const schema = type({
|
||||
projectDisplayName: "string",
|
||||
export const variablesSchema = type({
|
||||
count: "number"
|
||||
});
|
||||
|
||||
export function EmailTemplate({ projectDisplayName }: typeof schema.infer) {
|
||||
export function EmailTemplate({ user, variables }: Props<typeof variablesSchema.infer>) {
|
||||
return (
|
||||
<Container>
|
||||
<Subject value="Email Verification" />
|
||||
<Subject value={\`Hello \${user.displayName}!\`} />
|
||||
<NotificationCategory value="Transactional" />
|
||||
<div className="font-bold">Email Verification for { projectDisplayName }</div>
|
||||
<div className="font-bold">Hi {user.displayName}!</div>
|
||||
<br />
|
||||
count is {variables.count}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
EmailTemplate.PreviewVariables = {
|
||||
count: 10
|
||||
} satisfies typeof variablesSchema.infer
|
||||
\`\`\`
|
||||
|
||||
Here is the user's current email template:
|
||||
|
||||
@ -3,6 +3,7 @@ import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dis
|
||||
import { Result } from "@stackframe/stack-shared/dist/utils/results";
|
||||
import { deindent } from "@stackframe/stack-shared/dist/utils/strings";
|
||||
import { bundleJavaScript } from '@stackframe/stack-shared/dist/utils/esbuild';
|
||||
import { StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors';
|
||||
|
||||
export function createTemplateComponentFromHtml(
|
||||
html: string,
|
||||
@ -22,14 +23,29 @@ export function createTemplateComponentFromHtml(
|
||||
export async function renderEmailWithTemplate(
|
||||
templateComponent: string,
|
||||
themeComponent: string,
|
||||
variables: Record<string, string> = {},
|
||||
): Promise<Result<{ html: string, text: string, schema: any, subject?: string, notificationCategory?: string }, string>> {
|
||||
options: {
|
||||
user?: { displayName: string | null },
|
||||
project?: { displayName: string },
|
||||
variables?: Record<string, any>,
|
||||
previewMode?: boolean,
|
||||
},
|
||||
): Promise<Result<{ html: string, text: string, subject?: string, notificationCategory?: string }, string>> {
|
||||
const apiKey = getEnvVariable("STACK_FREESTYLE_API_KEY");
|
||||
const variables = options.variables ?? {};
|
||||
const previewMode = options.previewMode ?? false;
|
||||
const user = (previewMode && !options.user) ? { displayName: "John Doe" } : options.user;
|
||||
const project = (previewMode && !options.project) ? { displayName: "My Project" } : options.project;
|
||||
if (!user) {
|
||||
throw new StackAssertionError("User is required when not in preview mode", { user, project, variables });
|
||||
}
|
||||
if (!project) {
|
||||
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>`,
|
||||
schema: {},
|
||||
subject: "mock subject",
|
||||
notificationCategory: "mock notification category",
|
||||
});
|
||||
@ -40,20 +56,28 @@ export async function renderEmailWithTemplate(
|
||||
"/theme.tsx": themeComponent,
|
||||
"/template.tsx": templateComponent,
|
||||
"/render.tsx": deindent`
|
||||
import { configure } from "arktype/config"
|
||||
configure({ onUndeclaredKey: "delete" })
|
||||
import React from 'react';
|
||||
import * as TemplateModule from "./template.tsx";
|
||||
const { schema, EmailTemplate } = TemplateModule;
|
||||
import { findComponentValue } from "./utils.tsx";
|
||||
import { EmailTheme } from "./theme.tsx";
|
||||
import { render } from '@react-email/components';
|
||||
|
||||
import { type } from "arktype";
|
||||
import { findComponentValue } from "./utils.tsx";
|
||||
import * as TemplateModule from "./template.tsx";
|
||||
const { variablesSchema, EmailTemplate } = TemplateModule;
|
||||
import { EmailTheme } from "./theme.tsx";
|
||||
export const renderAll = async () => {
|
||||
const EmailTemplateWithProps = <EmailTemplate ${variablesAsProps} />;
|
||||
const variables = variablesSchema({
|
||||
${previewMode ? "...(EmailTemplate.PreviewVariables || {})," : ""}
|
||||
...(${JSON.stringify(variables)}),
|
||||
})
|
||||
if (variables instanceof type.errors) {
|
||||
throw new Error(variables.summary)
|
||||
}
|
||||
const EmailTemplateWithProps = <EmailTemplate variables={variables} user={${JSON.stringify(user)}} project={${JSON.stringify(project)}} />;
|
||||
const Email = <EmailTheme>{EmailTemplateWithProps}</EmailTheme>;
|
||||
return {
|
||||
html: await render(Email),
|
||||
text: await render(Email, { plainText: true }),
|
||||
schema: schema ? schema.toJsonSchema() : undefined,
|
||||
subject: findComponentValue(EmailTemplateWithProps, "Subject"),
|
||||
notificationCategory: findComponentValue(EmailTemplateWithProps, "NotificationCategory"),
|
||||
};
|
||||
@ -82,7 +106,7 @@ export async function renderEmailWithTemplate(
|
||||
if ("error" in output) {
|
||||
return Result.error(output.error as string);
|
||||
}
|
||||
return Result.ok(output.result as { html: string, text: string, schema: any, subject: string, notificationCategory: string });
|
||||
return Result.ok(output.result as { html: string, text: string, subject: string, notificationCategory: string });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { FormDialog } from "@/components/form-dialog";
|
||||
import { InputField } from "@/components/form-fields";
|
||||
import { useRouter } from "@/components/router";
|
||||
import { ActionDialog, Alert, AlertDescription, AlertTitle, Button, Card, Typography } from "@stackframe/stack-ui";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import * as yup from "yup";
|
||||
import { PageLayout } from "../page-layout";
|
||||
import { useAdminApp } from "../use-admin-app";
|
||||
|
||||
@ -16,7 +19,11 @@ export default function PageClient() {
|
||||
const [sharedSmtpWarningDialogOpen, setSharedSmtpWarningDialogOpen] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<PageLayout title="Email Templates" description="Customize the emails sent to your users">
|
||||
<PageLayout
|
||||
title="Email Templates"
|
||||
description="Customize the emails sent to your users"
|
||||
actions={<NewTemplateButton />}
|
||||
>
|
||||
{emailConfig?.type === 'shared' && <Alert variant="default">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
@ -71,3 +78,33 @@ export default function PageClient() {
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function NewTemplateButton() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const router = useRouter();
|
||||
|
||||
const handleCreateNewTemplate = async (values: { name: string }) => {
|
||||
const { id } = await stackAdminApp.createNewEmailTemplate(values.name);
|
||||
router.push(`email-templates-new/${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormDialog
|
||||
title="New Template"
|
||||
trigger={<Button>New Template</Button>}
|
||||
onSubmit={handleCreateNewTemplate}
|
||||
formSchema={yup.object({
|
||||
name: yup.string().defined(),
|
||||
})}
|
||||
render={(form) => (
|
||||
<InputField
|
||||
control={form.control}
|
||||
name="name"
|
||||
label="Template Name"
|
||||
placeholder="Enter template name"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -81,6 +81,15 @@ export default function CodeEditor({
|
||||
declare module "@stackframe/emails" {
|
||||
const Subject: React.FC<{value: string}>;
|
||||
const NotificationCategory: React.FC<{value: "Transactional" | "Marketing"}>;
|
||||
type Props<T> = {
|
||||
variables: T;
|
||||
project: {
|
||||
displayName: string;
|
||||
};
|
||||
user: {
|
||||
displayName: string | null;
|
||||
};
|
||||
};
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
@ -160,10 +160,9 @@ it("should render email when valid theme and template TSX sources are provided",
|
||||
export default EmailTheme;
|
||||
|
||||
|
||||
variables: {"projectDisplayName":"Stack Dashboard","teamDisplayName":"My Team","userDisplayName":"John Doe","emailVerificationLink":"<email verification link>","otp":"3SLSWZ","magicLink":"<magic link>","passwordResetLink":"<password reset link>","teamInvitationLink":"<team invitation link>","signInInvitationLink":"<sign in invitation link>"}</div>
|
||||
variables: {}</div>
|
||||
\`,
|
||||
"notification_category": "mock notification category",
|
||||
"schema": {},
|
||||
"subject": "mock subject",
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export const previewTemplateSource = `
|
||||
export const variablesSchema = v => v;
|
||||
export function EmailTemplate() {
|
||||
return (
|
||||
<div>
|
||||
@ -75,25 +76,39 @@ export const DEFAULT_EMAIL_THEMES = {
|
||||
},
|
||||
};
|
||||
|
||||
const EMAIL_TEMPLATE_EMAIL_VERIFICATION_ID = "e7d009ce-8d47-4528-b245-5bf119f2ffa3";
|
||||
const EMAIL_TEMPLATE_PASSWORD_RESET_ID = "a70fb3a4-56c1-4e42-af25-49d25603abd0";
|
||||
const EMAIL_TEMPLATE_MAGIC_LINK_ID = "822687fe-8d0a-4467-a0d1-416b6e639478";
|
||||
const EMAIL_TEMPLATE_TEAM_INVITATION_ID = "e84de395-2076-4831-9c19-8e9a96a868e4";
|
||||
const EMAIL_TEMPLATE_SIGN_IN_INVITATION_ID = "066dd73c-36da-4fd0-b6d6-ebf87683f8bc";
|
||||
|
||||
export const DEFAULT_EMAIL_TEMPLATES = {
|
||||
"e7d009ce-8d47-4528-b245-5bf119f2ffa3": {
|
||||
[EMAIL_TEMPLATE_EMAIL_VERIFICATION_ID]: {
|
||||
"displayName": "Email Verification",
|
||||
"tsxSource": "import { type } from \"arktype\"\nimport { Button, Section, Hr } from \"@react-email/components\";\nimport { Subject, NotificationCategory } from \"@stackframe/emails\";\n\nexport const schema = type({\n userDisplayName: \"string\",\n projectDisplayName: \"string\",\n emailVerificationLink: \"string\"\n})\n\nexport function EmailTemplate({ \n userDisplayName, \n projectDisplayName, \n emailVerificationLink \n}: typeof schema.infer) \n{\n return (\n <>\n <Subject value={`Verify your email at ${projectDisplayName}`} />\n <NotificationCategory value=\"Transactional\" />\n <div className=\"bg-white text-[#242424] font-sans text-base font-normal tracking-[0.15008px] leading-[1.5] m-0 py-8 w-full min-h-full\">\n <Section className=\"bg-white\">\n <h3 className=\"text-black font-sans font-bold text-[20px] text-center py-4 px-6 m-0\">\n Verify your email at {projectDisplayName}\n </h3>\n <p className=\"text-[#474849] font-sans font-normal text-[14px] text-center pt-2 px-6 pb-4 m-0\">\n Hi{userDisplayName ? (\", \" + userDisplayName) : ''}! Please click on the following button to verify your email.\n </p>\n <div className=\"text-center py-3 px-6\">\n <Button\n href={emailVerificationLink}\n target=\"_blank\"\n className=\"text-black font-sans font-bold text-[14px] inline-block bg-[#f0f0f0] rounded-[4px] py-3 px-5 no-underline border-0\"\n >\n Verify my email\n </Button>\n </div>\n <div className=\"py-4 px-6\">\n <Hr />\n </div>\n <p className=\"text-[#474849] font-sans font-normal text-[12px] text-center pt-1 px-6 pb-6 m-0\">\n If you were not expecting this email, you can safely ignore it. \n </p>\n </Section>\n </div>\n </>\n )\n}\n"
|
||||
"tsxSource": "import { type } from \"arktype\"\nimport { Button, Section, Hr } from \"@react-email/components\";\nimport { Subject, NotificationCategory, Props } from \"@stackframe/emails\";\n\nexport const variablesSchema = type({\n emailVerificationLink: \"string\"\n})\n\nexport function EmailTemplate({ user, project, variables }: Props<typeof variablesSchema.infer>) {\n return (\n <>\n <Subject value={`Verify your email at ${project.displayName}`} />\n <NotificationCategory value=\"Transactional\" />\n <div className=\"bg-white text-[#242424] font-sans text-base font-normal tracking-[0.15008px] leading-[1.5] m-0 py-8 w-full min-h-full\">\n <Section className=\"bg-white\">\n <h3 className=\"text-black font-sans font-bold text-[20px] text-center py-4 px-6 m-0\">\n Verify your email at {project.displayName}\n </h3>\n <p className=\"text-[#474849] font-sans font-normal text-[14px] text-center pt-2 px-6 pb-4 m-0\">\n Hi{user.displayName ? (\", \" + user.displayName) : ''}! Please click on the following button to verify your email.\n </p>\n <div className=\"text-center py-3 px-6\">\n <Button\n href={variables.emailVerificationLink}\n target=\"_blank\"\n className=\"text-black font-sans font-bold text-[14px] inline-block bg-[#f0f0f0] rounded-[4px] py-3 px-5 no-underline border-0\"\n >\n Verify my email\n </Button>\n </div>\n <div className=\"py-4 px-6\">\n <Hr />\n </div>\n <p className=\"text-[#474849] font-sans font-normal text-[12px] text-center pt-1 px-6 pb-6 m-0\">\n If you were not expecting this email, you can safely ignore it. \n </p>\n </Section>\n </div>\n </>\n )\n}\n\nEmailTemplate.PreviewVariables = {\n emailVerificationLink: \"<email verification link>\"\n} satisfies typeof variablesSchema.infer"
|
||||
},
|
||||
"a70fb3a4-56c1-4e42-af25-49d25603abd0": {
|
||||
[EMAIL_TEMPLATE_PASSWORD_RESET_ID]: {
|
||||
"displayName": "Password Reset",
|
||||
"tsxSource": "import { type } from \"arktype\"\nimport { Button, Section, Hr } from \"@react-email/components\"\nimport { Subject, NotificationCategory } from \"@stackframe/emails\"\n\nexport const schema = type({\n userDisplayName: \"string\",\n projectDisplayName: \"string\",\n passwordResetLink: \"string\"\n})\n\nexport function EmailTemplate({ userDisplayName, projectDisplayName, passwordResetLink }: typeof schema.infer) {\n return (\n <>\n <Subject value={\"Reset your password at \" + projectDisplayName} />\n <NotificationCategory value=\"Transactional\" />\n <div className=\"bg-white text-[#242424] font-sans text-base font-normal tracking-tight leading-relaxed py-8 w-full min-h-full\">\n <Section>\n <h3 className=\"text-black bg-transparent font-sans font-bold text-[20px] text-center py-4 px-6 m-0\">\n Reset your password at {projectDisplayName}\n </h3>\n\n <p className=\"text-[#474849] bg-transparent text-sm font-sans font-normal text-center pt-2 pb-4 px-6 m-0\">\n Hi{userDisplayName ? (\", \" + userDisplayName) : \"\"}! Please click on the following button to start the password reset process.\n </p>\n\n <div className=\"bg-transparent text-center px-6 py-3\">\n <Button\n href={passwordResetLink}\n className=\"text-black text-sm font-sans font-bold bg-[#f0f0f0] rounded-[4px] inline-block py-3 px-5 no-underline border-none\"\n target=\"_blank\"\n >\n Reset my password\n </Button>\n </div>\n\n <div className=\"px-6 py-4\">\n <Hr />\n </div>\n\n <p className=\"text-[#474849] bg-transparent text-xs font-sans font-normal text-center pt-1 pb-6 px-6 m-0\">\n If you were not expecting this email, you can safely ignore it.\n </p>\n </Section>\n </div>\n </>\n )\n}\n"
|
||||
"tsxSource": "import { type } from \"arktype\"\nimport { Button, Section, Hr } from \"@react-email/components\"\nimport { Subject, NotificationCategory, Props} from \"@stackframe/emails\"\n\nexport const variablesSchema = type({\n passwordResetLink: \"string\"\n})\n\nexport function EmailTemplate({ user, project, variables }: Props<typeof variablesSchema.infer>) {\n return (\n <>\n <Subject value={\"Reset your password at \" + project.displayName} />\n <NotificationCategory value=\"Transactional\" />\n <div className=\"bg-white text-[#242424] font-sans text-base font-normal tracking-tight leading-relaxed py-8 w-full min-h-full\">\n <Section>\n <h3 className=\"text-black bg-transparent font-sans font-bold text-[20px] text-center py-4 px-6 m-0\">\n Reset your password at {project.displayName}\n </h3>\n\n <p className=\"text-[#474849] bg-transparent text-sm font-sans font-normal text-center pt-2 pb-4 px-6 m-0\">\n Hi{user.displayName ? (\", \" + user.displayName) : \"\"}! Please click on the following button to start the password reset process.\n </p>\n\n <div className=\"bg-transparent text-center px-6 py-3\">\n <Button\n href={variables.passwordResetLink}\n className=\"text-black text-sm font-sans font-bold bg-[#f0f0f0] rounded-[4px] inline-block py-3 px-5 no-underline border-none\"\n target=\"_blank\"\n >\n Reset my password\n </Button>\n </div>\n\n <div className=\"px-6 py-4\">\n <Hr />\n </div>\n\n <p className=\"text-[#474849] bg-transparent text-xs font-sans font-normal text-center pt-1 pb-6 px-6 m-0\">\n If you were not expecting this email, you can safely ignore it.\n </p>\n </Section>\n </div>\n </>\n )\n}\n\nEmailTemplate.PreviewVariables = {\n passwordResetLink: \"<password reset link>\"\n} satisfies typeof variablesSchema.infer"
|
||||
},
|
||||
"822687fe-8d0a-4467-a0d1-416b6e639478": {
|
||||
[EMAIL_TEMPLATE_MAGIC_LINK_ID]: {
|
||||
"displayName": "Magic Link/OTP",
|
||||
"tsxSource": "import React from 'react';\nimport { type } from 'arktype';\nimport { Section, Hr } from '@react-email/components';\nimport { Subject, NotificationCategory } from '@stackframe/emails';\n\nexport const schema = type({\n userDisplayName: 'string',\n projectDisplayName: 'string',\n magicLink: 'string',\n otp: 'string',\n});\n\nexport function EmailTemplate({ userDisplayName, projectDisplayName, magicLink, otp }: typeof schema.infer) {\n return (\n <>\n <Subject value={\"Sign in to \" + projectDisplayName + \": Your code is \" + otp} />\n <NotificationCategory value=\"Transactional\" />\n <div className=\"bg-white text-[#242424] font-sans text-base font-normal tracking-[0.15008px] leading-6 m-0 py-8 w-full min-h-full\">\n <Section className=\"mx-auto bg-white\">\n <h3 className=\"text-black bg-transparent font-sans font-bold text-xl text-center px-6 py-4 m-0\">\n Sign in to {projectDisplayName}\n </h3>\n <p className=\"text-[#474849] bg-transparent text-sm font-sans font-normal text-center px-6 py-4 m-0\">\n Hi{userDisplayName ? \", \" + userDisplayName : \"\"}! This is your one-time-password for signing in:\n </p>\n <p className=\"text-black bg-transparent text-2xl font-mono font-bold text-center px-6 py-4 m-0\">\n {otp}\n </p>\n <p className=\"text-black bg-transparent text-sm font-sans font-normal text-center px-6 py-4 m-0\">\n Or you can click on{' '}\n <a\n key={20}\n href={magicLink}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-blue-600 underline\"\n >\n this link\n </a>{' '}\n to sign in\n </p>\n <Hr className=\"px-6 py-4 bg-transparent\" />\n <p className=\"text-[#474849] bg-transparent text-xs font-sans font-normal text-center px-6 pt-1 pb-6 m-0\">\n If you were not expecting this email, you can safely ignore it.\n </p>\n </Section>\n </div>\n </>\n );\n}\n"
|
||||
"tsxSource": "import { type } from 'arktype';\nimport { Section, Hr } from '@react-email/components';\nimport { Subject, NotificationCategory, Props } from '@stackframe/emails';\n\nexport const variablesSchema = type({\n magicLink: 'string',\n otp: 'string',\n});\n\nexport function EmailTemplate({ user, project, variables }: Props<typeof variablesSchema.infer>) {\n return (\n <>\n <Subject value={\"Sign in to \" + project.displayName + \": Your code is \" + variables.otp} />\n <NotificationCategory value=\"Transactional\" />\n <div className=\"bg-white text-[#242424] font-sans text-base font-normal tracking-[0.15008px] leading-6 m-0 py-8 w-full min-h-full\">\n <Section className=\"mx-auto bg-white\">\n <h3 className=\"text-black bg-transparent font-sans font-bold text-xl text-center px-6 py-4 m-0\">\n Sign in to {project.displayName}\n </h3>\n <p className=\"text-[#474849] bg-transparent text-sm font-sans font-normal text-center px-6 py-4 m-0\">\n Hi{user.displayName ? \", \" + user.displayName : \"\"}! This is your one-time-password for signing in:\n </p>\n <p className=\"text-black bg-transparent text-2xl font-mono font-bold text-center px-6 py-4 m-0\">\n {variables.otp}\n </p>\n <p className=\"text-black bg-transparent text-sm font-sans font-normal text-center px-6 py-4 m-0\">\n Or you can click on{' '}\n <a\n key={20}\n href={variables.magicLink}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"text-blue-600 underline\"\n >\n this link\n </a>{' '}\n to sign in\n </p>\n <Hr className=\"px-6 py-4 bg-transparent\" />\n <p className=\"text-[#474849] bg-transparent text-xs font-sans font-normal text-center px-6 pt-1 pb-6 m-0\">\n If you were not expecting this email, you can safely ignore it.\n </p>\n </Section>\n </div>\n </>\n );\n}\n\nEmailTemplate.PreviewVariables = {\n magicLink: \"<magic link>\",\n otp: \"3SLSWZ\"\n} satisfies typeof variablesSchema.infer"
|
||||
},
|
||||
"066dd73c-36da-4fd0-b6d6-ebf87683f8bc": {
|
||||
[EMAIL_TEMPLATE_TEAM_INVITATION_ID]: {
|
||||
"displayName": "Team Invitation",
|
||||
"tsxSource": "import { type } from \"arktype\";\nimport { Button, Section, Hr } from \"@react-email/components\";\nimport { Subject, NotificationCategory } from \"@stackframe/emails\";\n\n\nexport const schema = type({\n userDisplayName: \"string\",\n teamDisplayName: \"string\",\n teamInvitationLink: \"string\"\n});\n\nexport function EmailTemplate({ userDisplayName, teamDisplayName, teamInvitationLink }: typeof schema.infer) {\n return (\n <>\n <Subject value={\"You have been invited to join \" + teamDisplayName} />\n <NotificationCategory value=\"Transactional\" />\n <div className=\"bg-white text-[#242424] font-sans text-base font-normal tracking-[0.15008px] leading-[1.5] m-0 py-8 w-full min-h-full\">\n <Section className=\"mx-auto max-w-lg bg-white\">\n <h3 className=\"text-black bg-transparent font-sans font-bold text-xl text-center px-6 pt-8 m-0\">\n You are invited to {teamDisplayName}\n </h3>\n <p className=\"text-[#474849] bg-transparent text-sm font-sans font-normal text-center px-6 pt-2 pb-4 m-0\">\n Hi{userDisplayName ? \", \" + userDisplayName : \"\"}! Please click the button below to join the team {teamDisplayName}\n </p>\n <div className=\"bg-transparent text-center px-6 py-3\">\n <Button\n href={teamInvitationLink}\n target=\"_blank\"\n className=\"text-black text-sm font-sans font-bold bg-[#f0f0f0] rounded-md inline-block px-5 py-3 no-underline border-0\"\n >\n Join team\n </Button>\n </div>\n <div className=\"px-6 py-4 bg-transparent\">\n <Hr />\n </div>\n <p className=\"text-[#474849] bg-transparent text-xs font-sans font-normal text-center px-6 pb-6 pt-1 m-0\">\n If you were not expecting this email, you can safely ignore it.\n </p>\n </Section>\n </div>\n </>\n );\n}\n"
|
||||
"tsxSource": "import { type } from \"arktype\";\nimport { Button, Section, Hr } from \"@react-email/components\";\nimport { Subject, NotificationCategory, Props } from \"@stackframe/emails\";\n\n\nexport const variablesSchema = type({\n teamDisplayName: \"string\",\n teamInvitationLink: \"string\"\n});\n\nexport function EmailTemplate({ user, variables }: Props<typeof variablesSchema.infer>) {\n return (\n <>\n <Subject value={\"You have been invited to join \" + variables.teamDisplayName} />\n <NotificationCategory value=\"Transactional\" />\n <div className=\"bg-white text-[#242424] font-sans text-base font-normal tracking-[0.15008px] leading-[1.5] m-0 py-8 w-full min-h-full\">\n <Section className=\"mx-auto max-w-lg bg-white\">\n <h3 className=\"text-black bg-transparent font-sans font-bold text-xl text-center px-6 pt-8 m-0\">\n You are invited to {variables.teamDisplayName}\n </h3>\n <p className=\"text-[#474849] bg-transparent text-sm font-sans font-normal text-center px-6 pt-2 pb-4 m-0\">\n Hi{user.displayName ? \", \" + user.displayName : \"\"}! Please click the button below to join the team {variables.teamDisplayName}\n </p>\n <div className=\"bg-transparent text-center px-6 py-3\">\n <Button\n href={variables.teamInvitationLink}\n target=\"_blank\"\n className=\"text-black text-sm font-sans font-bold bg-[#f0f0f0] rounded-md inline-block px-5 py-3 no-underline border-0\"\n >\n Join team\n </Button>\n </div>\n <div className=\"px-6 py-4 bg-transparent\">\n <Hr />\n </div>\n <p className=\"text-[#474849] bg-transparent text-xs font-sans font-normal text-center px-6 pb-6 pt-1 m-0\">\n If you were not expecting this email, you can safely ignore it.\n </p>\n </Section>\n </div>\n </>\n );\n}\n\nEmailTemplate.PreviewVariables = {\n teamDisplayName: \"My Team\",\n teamInvitationLink: \"<team invitation link>\"\n} satisfies typeof variablesSchema.infer "
|
||||
},
|
||||
"e84de395-2076-4831-9c19-8e9a96a868e4": {
|
||||
[EMAIL_TEMPLATE_SIGN_IN_INVITATION_ID]: {
|
||||
"displayName": "Sign In Invitation",
|
||||
"tsxSource": "import { type } from \"arktype\"\nimport { Button, Section, Hr } from \"@react-email/components\";\nimport { Subject, NotificationCategory } from \"@stackframe/emails\";\n\nexport const schema = type({\n userDisplayName: \"string\",\n projectDisplayName: \"string\",\n signInInvitationLink: \"string\",\n teamDisplayName: \"string\"\n})\n\nexport function EmailTemplate({\n userDisplayName,\n projectDisplayName,\n signInInvitationLink,\n teamDisplayName\n}: typeof schema.infer) {\n return (\n <>\n <Subject\n value={\"You have been invited to sign in to \" + projectDisplayName}\n />\n <NotificationCategory value=\"Transactional\" />\n\n <div className=\"bg-white text-gray-900 font-sans text-base font-normal leading-normal w-full min-h-full m-0 py-8\">\n <Section>\n <h3 className=\"text-black bg-transparent font-sans font-bold text-xl text-center pt-8 px-6 m-0\">\n You are invited to sign in to {teamDisplayName}\n </h3>\n\n <p className=\"text-gray-700 bg-transparent text-sm font-sans font-normal text-center pt-2 pb-4 px-6 m-0\">\n Hi\n {userDisplayName ? \", \" + userDisplayName : \"\"}! Please click on the following\n link to sign in to your account\n </p>\n\n <div className=\"bg-transparent text-center px-6 py-3\">\n <Button\n href={signInInvitationLink}\n className=\"text-black text-sm font-sans font-bold bg-gray-200 rounded-md inline-block py-3 px-5 no-underline border-none\"\n target=\"_blank\"\n >\n Sign in\n </Button>\n </div>\n\n <div className=\"px-6 py-4 bg-transparent\">\n <Hr />\n </div>\n\n <p className=\"text-gray-700 bg-transparent text-xs font-sans font-normal text-center pt-1 pb-6 px-6 m-0\">\n If you were not expecting this email, you can safely ignore it.\n </p>\n </Section>\n </div>\n </>\n )\n}\n"
|
||||
"tsxSource": "import { type } from \"arktype\"\nimport { Button, Section, Hr } from \"@react-email/components\";\nimport { Subject, NotificationCategory, Props } from \"@stackframe/emails\";\n\nexport const variablesSchema = type({\n signInInvitationLink: \"string\",\n teamDisplayName: \"string\"\n})\n\nexport function EmailTemplate({ user, project, variables }: Props<typeof variablesSchema.infer>) {\n return (\n <>\n <Subject\n value={\"You have been invited to sign in to \" + project.displayName}\n />\n <NotificationCategory value=\"Transactional\" />\n\n <div className=\"bg-white text-gray-900 font-sans text-base font-normal leading-normal w-full min-h-full m-0 py-8\">\n <Section>\n <h3 className=\"text-black bg-transparent font-sans font-bold text-xl text-center pt-8 px-6 m-0\">\n You are invited to sign in to {variables.teamDisplayName}\n </h3>\n\n <p className=\"text-gray-700 bg-transparent text-sm font-sans font-normal text-center pt-2 pb-4 px-6 m-0\">\n Hi\n {user.displayName ? \", \" + user.displayName : \"\"}! Please click on the following\n link to sign in to your account\n </p>\n\n <div className=\"bg-transparent text-center px-6 py-3\">\n <Button\n href={variables.signInInvitationLink}\n className=\"text-black text-sm font-sans font-bold bg-gray-200 rounded-md inline-block py-3 px-5 no-underline border-none\"\n target=\"_blank\"\n >\n Sign in\n </Button>\n </div>\n\n <div className=\"px-6 py-4 bg-transparent\">\n <Hr />\n </div>\n\n <p className=\"text-gray-700 bg-transparent text-xs font-sans font-normal text-center pt-1 pb-6 px-6 m-0\">\n If you were not expecting this email, you can safely ignore it.\n </p>\n </Section>\n </div>\n </>\n )\n}\n\nEmailTemplate.PreviewVariables = {\n signInInvitationLink: \"<sign in invitation link>\",\n teamDisplayName: \"My Team\"\n} satisfies typeof variablesSchema.infer"
|
||||
}
|
||||
};
|
||||
|
||||
export const DEFAULT_TEMPLATE_IDS = {
|
||||
email_verification: EMAIL_TEMPLATE_EMAIL_VERIFICATION_ID,
|
||||
password_reset: EMAIL_TEMPLATE_PASSWORD_RESET_ID,
|
||||
magic_link: EMAIL_TEMPLATE_MAGIC_LINK_ID,
|
||||
team_invitation: EMAIL_TEMPLATE_TEAM_INVITATION_ID,
|
||||
sign_in_invitation: EMAIL_TEMPLATE_SIGN_IN_INVITATION_ID,
|
||||
} as const;
|
||||
|
||||
@ -472,6 +472,23 @@ export class StackAdminInterface extends StackServerInterface {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async createNewEmailTemplate(displayName: string): Promise<{ id: string }> {
|
||||
const response = await this.sendAdminRequest(
|
||||
`/internal/email-templates`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
display_name: displayName,
|
||||
}),
|
||||
},
|
||||
null,
|
||||
);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async getAllProjectsIdsForMigration(cursor?: string): Promise<{ project_ids: string[], next_cursor: string | null }> {
|
||||
const queryParams = cursor ? `?cursor=${encodeURIComponent(cursor)}` : '';
|
||||
const response = await this.sendAdminRequest(
|
||||
|
||||
@ -456,6 +456,12 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
|
||||
await this._interface.sendSignInInvitationEmail(email, callbackUrl);
|
||||
}
|
||||
|
||||
async createNewEmailTemplate(displayName: string): Promise<{ id: string }> {
|
||||
const result = await this._interface.createNewEmailTemplate(displayName);
|
||||
await this._adminNewEmailTemplatesCache.refresh([]);
|
||||
return result;
|
||||
}
|
||||
|
||||
async sendChatMessage(
|
||||
threadId: string,
|
||||
contextType: "email-theme" | "email-template",
|
||||
|
||||
@ -83,6 +83,7 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
|
||||
saveChatMessage(threadId: string, message: any): Promise<void>,
|
||||
listChatMessages(threadId: string): Promise<{ messages: Array<any> }>,
|
||||
updateNewEmailTemplate(id: string, tsxSource: string): Promise<{ renderedHtml: string }>,
|
||||
createNewEmailTemplate(displayName: string): Promise<{ id: string }>,
|
||||
|
||||
getAllProjectsIdsForMigration(cursor?: string): Promise<{ projectIds: string[], nextCursor: string | null }>,
|
||||
convertEmailTemplates(projectId: string): Promise<{ templatesConverted: number, totalTemplates: number, rendered: Array<{ legacyTemplateContent: any, templateType: string, renderedHtml: string | null }> }>,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user