Skip to content

Commit

Permalink
Add email support
Browse files Browse the repository at this point in the history
  • Loading branch information
salyh committed Dec 25, 2024
1 parent 7ff6b0d commit 86e6221
Show file tree
Hide file tree
Showing 13 changed files with 20,179 additions and 1,129 deletions.
28 changes: 28 additions & 0 deletions emails/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { GetMessages } from "keycloakify-emails";

/**
* we want to have this as function with a params, to give developers a
* flexibility to initialize theirs own i18n solution here
*/
export const getMessages: GetMessages = (props) => {
// this default properties are optional, if you omit them, they will be taken from a base theme
return {
"requiredAction.CONFIGURE_TOTP": "Configure OTP",
"requiredAction.TERMS_AND_CONDITIONS": "Terms and Conditions",
"requiredAction.UPDATE_PASSWORD": "Update Password",
"requiredAction.UPDATE_PROFILE": "Update Profile",
"requiredAction.VERIFY_EMAIL": "Verify Email",
"requiredAction.CONFIGURE_RECOVERY_AUTHN_CODES": "Generate Recovery Codes",

// # units for link expiration timeout formatting
// # for languages which have more unit plural forms depending on the value (eg. Czech and other Slavic langs) you can override unit text for some other values like described in the Java choice format which is documented here. For Czech, it would be '{0,choice,0#minut|1#minuta|2#minuty|2<minut}'
// # https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/text/MessageFormat.html
// # https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/text/ChoiceFormat.html
"linkExpirationFormatter.timePeriodUnit.seconds":
"{0,choice,0#seconds|1#second|1<seconds}",
"linkExpirationFormatter.timePeriodUnit.minutes":
"{0,choice,0#minutes|1#minute|1<minutes}",
"linkExpirationFormatter.timePeriodUnit.hours": "{0,choice,0#hours|1#hour|1<hours}",
"linkExpirationFormatter.timePeriodUnit.days": "{0,choice,0#days|1#day|1<days}",
};
};
37 changes: 37 additions & 0 deletions emails/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Body, Container, Head, Html, Preview, Section } from "jsx-email";
import { PropsWithChildren, ReactNode } from "react";

const main = {
backgroundColor: "#f6f9fc",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};

const container = {
backgroundColor: "#ffffff",
margin: "0 auto",
marginBottom: "64px",
padding: "20px 0 48px",
};

const box = {
padding: "0 48px",
};

export const EmailLayout = ({
locale,
children,
preview,
}: PropsWithChildren<{ preview: ReactNode; locale: string }>) => {
return (
<Html lang={locale}>
<Head />
<Preview>{preview}</Preview>
<Body style={main}>
<Container style={container}>
<Section style={box}>{children}</Section>
</Container>
</Body>
</Html>
);
};
Binary file added emails/templates/assets/kc-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions emails/templates/email-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { render, Text, Img, Container } from "jsx-email";
import { EmailLayout } from "../layout";
import {
createVariablesHelper,
GetSubject,
GetTemplate,
GetTemplateProps,
} from "keycloakify-emails";

interface TemplateProps extends Omit<GetTemplateProps, "plainText"> {}

const paragraph = {
color: "#777",
fontSize: "16px",
lineHeight: "24px",
textAlign: "left" as const,
};

export const previewProps: TemplateProps = {
locale: "en",
themeName: "vanilla",
};

export const templateName = "Email Test";

const { exp } = createVariablesHelper("email-test.ftl");

const baseUrl = import.meta.isJsxEmailPreview ? "/assets" : "${url.resourcesUrl}";

export const Template = ({ locale }: TemplateProps) => (
<EmailLayout preview={"Here is a preview"} locale={locale}>
<Container>
<Img src={`${baseUrl}/kc-logo.png`} alt="KC Logo" width="83" height="75" />
</Container>
<Text style={paragraph}>This is a test message from {exp("realmName")}</Text>
</EmailLayout>
);

export const getTemplate: GetTemplate = async (props) => {
return await render(<Template {...props} />, { plainText: props.plainText });
};

export const getSubject: GetSubject = async (_props) => {
return "[KEYCLOAK] - SMTP test message";
};
53 changes: 53 additions & 0 deletions emails/templates/email-update-confirmation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Text, render } from "jsx-email";
import { EmailLayout } from "../layout";
import {
GetSubject,
GetTemplate,
GetTemplateProps,
createVariablesHelper,
} from "keycloakify-emails";
interface TemplateProps extends Omit<GetTemplateProps, "plainText"> {}

const paragraph = {
color: "#777",
fontSize: "16px",
lineHeight: "24px",
textAlign: "left" as const,
};

export const previewProps: TemplateProps = {
locale: "en",
themeName: "vanilla",
};

export const templateName = "Email Update Confirmation";

const { exp } = createVariablesHelper("email-update-confirmation.ftl");

export const Template = ({ locale }: TemplateProps) => (
<EmailLayout preview={`Here is a preview`} locale={locale}>
<Text style={paragraph}>
<p>
To update your {exp("realmName")} account with email address {exp("newEmail")},
click the link below
</p>
<p>
<a href={exp("link")}>{exp("link")}</a>
</p>
<p>
This link will expire within {exp("linkExpirationFormatter(linkExpiration)")}.
</p>
<p>
If you don't want to proceed with this modification, just ignore this message.
</p>
</Text>
</EmailLayout>
);

export const getTemplate: GetTemplate = async (props) => {
return await render(<Template {...props} />, { plainText: props.plainText });
};

export const getSubject: GetSubject = async (_props) => {
return "Verify new email";
};
52 changes: 52 additions & 0 deletions emails/templates/email-verification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Text, render } from "jsx-email";
import { EmailLayout } from "../layout";
import {
createVariablesHelper,
GetSubject,
GetTemplate,
GetTemplateProps,
} from "keycloakify-emails";

interface TemplateProps extends Omit<GetTemplateProps, "plainText"> {}

const paragraph = {
color: "#777",
fontSize: "16px",
lineHeight: "24px",
textAlign: "left" as const,
};

export const previewProps: TemplateProps = {
locale: "en",
themeName: "vanilla",
};

export const templateName = "Email Verification";

const { exp } = createVariablesHelper("email-verification.ftl");

export const Template = ({ locale }: TemplateProps) => (
<EmailLayout preview={`Here is a preview`} locale={locale}>
<Text style={paragraph}>
<p>
Someone has created a {exp("user.firstName")} account with this email address. If
this was you, click the link below to verify your email address
</p>
<p>
<a href={exp("link")}>Link to e-mail address verification</a>
</p>
<p>
This link will expire within {exp("linkExpirationFormatter(linkExpiration)")}.
</p>
<p>If you didn't create this account, just ignore this message.</p>
</Text>
</EmailLayout>
);

export const getTemplate: GetTemplate = async (props) => {
return await render(<Template {...props} />, { plainText: props.plainText });
};

export const getSubject: GetSubject = async (_props) => {
return "Verify email";
};
59 changes: 59 additions & 0 deletions emails/templates/org-invite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Text, render } from "jsx-email";
import { EmailLayout } from "../layout";
import * as Fm from "keycloakify-emails/jsx-email";
import {
createVariablesHelper,
GetSubject,
GetTemplate,
GetTemplateProps,
} from "keycloakify-emails";

interface TemplateProps extends Omit<GetTemplateProps, "plainText"> {}

const paragraph = {
color: "#777",
fontSize: "16px",
lineHeight: "24px",
textAlign: "left" as const,
};

export const previewProps: TemplateProps = {
locale: "en",
themeName: "vanilla",
};

export const templateName = "Org Invite";

const { exp, v } = createVariablesHelper("org-invite.ftl");

export const Template = ({ locale }: TemplateProps) => (
<EmailLayout preview={`Here is a preview`} locale={locale}>
<Text style={paragraph}>
<Fm.If condition={`${v("firstName")}?? && ${v("lastName")}??`}>
<p>
Hi, {exp("firstName")} {exp("lastName")}.
</p>
</Fm.If>

<p>
You were invited to join the {exp("organization.name")} organization. Click the
link below to join.{" "}
</p>
<p>
<a href={exp("link")}>Link to join the organization</a>
</p>
<p>
This link will expire within {exp("linkExpirationFormatter(linkExpiration)")}.
</p>
<p>If you don't want to join the organization, just ignore this message.</p>
</Text>
</EmailLayout>
);

export const getTemplate: GetTemplate = async (props) => {
return await render(<Template {...props} />, { plainText: props.plainText });
};

export const getSubject: GetSubject = async (_props) => {
return "Invitation to join the {0} organization";
};
54 changes: 54 additions & 0 deletions emails/templates/password-reset.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Text, render } from "jsx-email";
import { EmailLayout } from "../layout";
import {
createVariablesHelper,
GetSubject,
GetTemplate,
GetTemplateProps,
} from "keycloakify-emails";

interface TemplateProps extends Omit<GetTemplateProps, "plainText"> {}

const paragraph = {
color: "#777",
fontSize: "16px",
lineHeight: "24px",
textAlign: "left" as const,
};

export const previewProps: TemplateProps = {
locale: "en",
themeName: "vanilla",
};

export const templateName = "Password Reset";

const { exp } = createVariablesHelper("password-reset.ftl");

export const Template = ({ locale }: TemplateProps) => (
<EmailLayout preview={`Here is a preview`} locale={locale}>
<Text style={paragraph}>
<p>
Someone just requested to change your {exp("realmName")} account's credentials. If
this was you, click on the link below to reset them.
</p>
<p>
<a href={exp("link")}>Link to reset credentials</a>
</p>
<p>
This link will expire within {exp("linkExpirationFormatter(linkExpiration)")}.
</p>
<p>
If you don't want to reset your credentials, just ignore this message and nothing
will be changed.
</p>
</Text>
</EmailLayout>
);
export const getTemplate: GetTemplate = async (props) => {
return await render(<Template {...props} />, { plainText: props.plainText });
};

export const getSubject: GetSubject = async (_props) => {
return "Reset password";
};
Loading

0 comments on commit 86e6221

Please sign in to comment.