Skip to content

Commit

Permalink
Improve email templates
Browse files Browse the repository at this point in the history
Ensure that we explicitly set the color property because some email clients (icloud) will not render text properly if not set

Increased email verification code to 48 hours

Ensure code is populated in form from url query params

Work towards #1076
  • Loading branch information
paustint committed Nov 17, 2024
1 parent 8f31d0f commit 134e28b
Show file tree
Hide file tree
Showing 16 changed files with 73 additions and 120 deletions.
10 changes: 6 additions & 4 deletions apps/api/src/app/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createRememberDevice,
createUserActivityFromReq,
createUserActivityFromReqWithError,
EMAIL_VERIFICATION_TOKEN_DURATION_HOURS,
ensureAuthError,
ExpiredVerificationToken,
generatePasswordResetToken,
Expand Down Expand Up @@ -41,7 +42,7 @@ import {
} from '@jetstream/email';
import { ensureBoolean } from '@jetstream/shared/utils';
import { parse as parseCookie } from 'cookie';
import { addMinutes } from 'date-fns';
import { addHours, addMinutes } from 'date-fns';
import { z } from 'zod';
import { Request } from '../types/types';
import { redirect, sendJson, setCsrfCookie } from '../utils/response.handlers';
Expand Down Expand Up @@ -180,15 +181,16 @@ function initSession(
req.session.pendingVerification = null;

if (verificationRequired) {
const exp = addMinutes(new Date(), TOKEN_DURATION_MINUTES).getTime();
const token = generateRandomCode(6);
if (isNewUser) {
req.session.sendNewUserEmailAfterVerify = true;
}
if (verificationRequired.email) {
const exp = addHours(new Date(), EMAIL_VERIFICATION_TOKEN_DURATION_HOURS).getTime();
// If email verification is required, we can consider that as 2fa as well, so do not need to combine with other 2fa factors
req.session.pendingVerification = [{ type: 'email', exp, token }];
} else if (verificationRequired.twoFactor?.length > 0) {
const exp = addMinutes(new Date(), TOKEN_DURATION_MINUTES).getTime();
req.session.pendingVerification = verificationRequired.twoFactor.map((factor) => {
switch (factor.type) {
case '2fa-otp':
Expand Down Expand Up @@ -451,7 +453,7 @@ const callback = createRoute(routeDefinition.callback.validators, async ({ body,
const initialVerification = req.session.pendingVerification[0];

if (initialVerification.type === 'email') {
await sendEmailVerification(req.session.user.email, initialVerification.token, TOKEN_DURATION_MINUTES);
await sendEmailVerification(req.session.user.email, initialVerification.token, EMAIL_VERIFICATION_TOKEN_DURATION_HOURS);
} else if (initialVerification.type === '2fa-email') {
await sendVerificationCode(req.session.user.email, initialVerification.token, TOKEN_DURATION_MINUTES);
}
Expand Down Expand Up @@ -621,7 +623,7 @@ const resendVerification = createRoute(routeDefinition.resendVerification.valida

switch (type) {
case 'email': {
await sendEmailVerification(req.session.user.email, token, TOKEN_DURATION_MINUTES);
await sendEmailVerification(req.session.user.email, token, EMAIL_VERIFICATION_TOKEN_DURATION_HOURS);
break;
}
case '2fa-email': {
Expand Down
3 changes: 2 additions & 1 deletion apps/landing/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import Link from 'next/link';

const footerNavigation = {
support: [
{ name: 'Documentation', href: 'https://docs.getjetstream.app/', target: '_blank' },
{ name: 'Documentation', href: 'https://docs.getjetstream.app', target: '_blank' },
{ name: 'Status', href: 'https://status.getjetstream.app', target: '_blank' },
{ name: 'Ask a question', href: 'https://discord.gg/sfxd', target: '_blank' },
{ name: 'File an issue', href: 'https://github.com/jetstreamapp/jetstream/issues', target: '_blank' },
{ name: 'Contact Us', href: 'mailto:[email protected]', target: '_blank' },
Expand Down
4 changes: 3 additions & 1 deletion apps/landing/components/auth/VerifyEmailOr2fa.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { TwoFactorType } from '@jetstream/auth/types';
import { Maybe } from '@jetstream/types';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { useRouter } from 'next/router';
import { FormEvent, Fragment, useState } from 'react';
import { useForm } from 'react-hook-form';
Expand Down Expand Up @@ -47,6 +48,7 @@ interface VerifyEmailOr2faProps {

export function VerifyEmailOr2fa({ csrfToken, email, pendingVerifications }: VerifyEmailOr2faProps) {
const router = useRouter();
const searchParams = useSearchParams();
const [error, setError] = useState<string>();
const [hasResent, setHasResent] = useState(false);

Expand All @@ -61,7 +63,7 @@ export function VerifyEmailOr2fa({ csrfToken, email, pendingVerifications }: Ver
} = useForm({
resolver: zodResolver(FormSchema),
defaultValues: {
code: '',
code: searchParams.get('code') || '',
csrfToken,
captchaToken: '',
type: activeFactor,
Expand Down
14 changes: 6 additions & 8 deletions libs/api-config/src/lib/email.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ export async function sendEmail({
...rest,
});

if (results.id) {
prisma.emailActivity
.create({
data: { email: to, subject, status: `${results.status || ''}`, providerId: results.id },
select: { id: true },
})
.catch((err) => logger.error({ message: err?.message }, '[EMAIL][ERROR] Error logging email activity'));
}
prisma.emailActivity
.create({
data: { email: to, subject, status: `${results.status}` || null, providerId: results.id },
select: { id: true },
})
.catch((err) => logger.error({ message: err?.message }, '[EMAIL][ERROR] Error logging email activity'));
}
1 change: 1 addition & 0 deletions libs/auth/server/src/lib/auth.constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const PASSWORD_RESET_DURATION_MINUTES = 30;
export const TOKEN_DURATION_MINUTES = 15;
export const EMAIL_VERIFICATION_TOKEN_DURATION_HOURS = 48;

export const DELETE_ACTIVITY_DAYS = 30;
export const DELETE_TOKEN_DAYS = 3;
2 changes: 0 additions & 2 deletions libs/email/src/lib/components/EmailFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const EmailFooter = () => {
fontSize: 14,
lineHeight: '18px',
fontWeight: 600,
color: 'rgb(17,24,39)',
textTransform: 'uppercase',
}}
>
Expand Down Expand Up @@ -49,7 +48,6 @@ export const EmailFooter = () => {
lineHeight: '18px',
fontWeight: 600,
color: 'rgb(107,114,128)',
textTransform: 'uppercase',
}}
>
[email protected]
Expand Down
14 changes: 14 additions & 0 deletions libs/email/src/lib/components/EmailLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Img } from '@react-email/components';
import * as React from 'react';
import { EMAIL_STYLES } from '../shared-styles';

export const EmailLogo = () => {
return (
<Img
src="https://res.cloudinary.com/getjetstream/image/upload/v1634516631/public/jetstream-logo-200w.png"
width="200"
alt="Jetstream logo"
style={EMAIL_STYLES.logo}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Body, Container, Head, Heading, Html, Img, Preview, Section, Text } from '@react-email/components';
import { Body, Container, Head, Heading, Html, Preview, Section, Text } from '@react-email/components';
import * as React from 'react';
import { EmailFooter } from '../../components/EmailFooter';
import { EmailLogo } from '../../components/EmailLogo';
import { EMAIL_STYLES } from '../../shared-styles';

export interface AuthenticationChangeConfirmationEmailProps {
Expand All @@ -20,12 +21,7 @@ export const AuthenticationChangeConfirmationEmail = ({
<Preview>{preview}</Preview>
<Body style={EMAIL_STYLES.main}>
<Container style={EMAIL_STYLES.container}>
<Img
src="https://res.cloudinary.com/getjetstream/image/upload/v1634516631/public/jetstream-logo-200w.png"
width="200"
alt="Jetstream"
style={EMAIL_STYLES.logo}
/>
<EmailLogo />
<Heading style={EMAIL_STYLES.codeTitle}>{heading}</Heading>

{!!additionalTextSegments?.length && (
Expand Down
37 changes: 6 additions & 31 deletions libs/email/src/lib/email-templates/auth/GenericEmail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Body, Container, Head, Heading, Html, Img, Preview, Section, Text } from '@react-email/components';
import { Body, Container, Head, Heading, Html, Preview, Section, Text } from '@react-email/components';
import * as React from 'react';
import { EmailFooter } from '../../components/EmailFooter';
import { EmailLogo } from '../../components/EmailLogo';
import { EMAIL_STYLES } from '../../shared-styles';

interface GenericEmailProps {
Expand All @@ -13,15 +14,10 @@ export const GenericEmail = ({ preview, heading, segments }: GenericEmailProps)
<Html>
<Head />
<Preview>{preview}</Preview>
<Body style={main}>
<Container style={container}>
<Img
src="https://res.cloudinary.com/getjetstream/image/upload/v1634516631/public/jetstream-logo-200w.png"
width="200"
alt="Jetstream"
style={EMAIL_STYLES.logo}
/>
<Heading style={title}>{heading}</Heading>
<Body style={EMAIL_STYLES.main}>
<Container style={EMAIL_STYLES.container}>
<EmailLogo />
<Heading style={EMAIL_STYLES.title}>{heading}</Heading>

<Section style={{ marginTop: 16, marginBottom: 16 }}>
{segments.map((text, index) => (
Expand All @@ -44,27 +40,6 @@ GenericEmail.PreviewProps = {
segments: ['can you do xyz?', 'yes, we can do xyz!'],
} as GenericEmailProps;

const main: React.CSSProperties = {
backgroundColor: '#ffffff',
fontFamily:
'-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol',
};

const container: React.CSSProperties = {
backgroundColor: '#ffffff',
border: '1px solid #ddd',
borderRadius: '5px',
marginTop: '20px',
width: '710px',
maxWidth: '100%',
margin: '0 auto',
padding: '5% 3%',
};

const title: React.CSSProperties = {
textAlign: 'center' as const,
};

const sectionText: React.CSSProperties = {
margin: '0px',
fontSize: 14,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Body, Container, Head, Heading, Html, Img, Preview, Text } from '@react-email/components';
import { Body, Container, Head, Heading, Html, Preview, Text } from '@react-email/components';
import * as React from 'react';
import { EmailFooter } from '../../components/EmailFooter';
import { EmailLogo } from '../../components/EmailLogo';
import { EMAIL_STYLES } from '../../shared-styles';

export const PasswordResetConfirmationEmail = () => {
Expand All @@ -10,12 +11,7 @@ export const PasswordResetConfirmationEmail = () => {
<Preview>Your password has been reset</Preview>
<Body style={EMAIL_STYLES.main}>
<Container style={EMAIL_STYLES.container}>
<Img
src="https://res.cloudinary.com/getjetstream/image/upload/v1634516631/public/jetstream-logo-200w.png"
width="200"
alt="Jetstream"
style={EMAIL_STYLES.logo}
/>
<EmailLogo />
<Heading style={EMAIL_STYLES.codeTitle}>Your password has been successfully reset</Heading>

<Text style={EMAIL_STYLES.paragraphHeading}>Didn't request this?</Text>
Expand Down
10 changes: 3 additions & 7 deletions libs/email/src/lib/email-templates/auth/PasswordResetEmail.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Body, Button, Container, Head, Heading, Html, Img, Link, Preview, Section, Text } from '@react-email/components';
import { Body, Button, Container, Head, Heading, Html, Link, Preview, Section, Text } from '@react-email/components';
import * as React from 'react';
import { EmailFooter } from '../../components/EmailFooter';
import { EmailLogo } from '../../components/EmailLogo';
import { EMAIL_STYLES } from '../../shared-styles';

interface PasswordResetEmailProps {
Expand All @@ -24,12 +25,7 @@ export const PasswordResetEmail = ({
<Preview>Reset your password with Jetstream</Preview>
<Body style={EMAIL_STYLES.main}>
<Container style={EMAIL_STYLES.container}>
<Img
src="https://res.cloudinary.com/getjetstream/image/upload/v1634516631/public/jetstream-logo-200w.png"
width="200"
alt="Jetstream"
style={EMAIL_STYLES.logo}
/>
<EmailLogo />
<Heading style={EMAIL_STYLES.codeTitle}>Reset your password</Heading>

<Text style={EMAIL_STYLES.codeDescription}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Body, Container, Head, Heading, Html, Img, Preview, Section, Text } from '@react-email/components';
import { Body, Container, Head, Heading, Html, Preview, Section, Text } from '@react-email/components';
import * as React from 'react';
import { EmailFooter } from '../../components/EmailFooter';
import { EmailLogo } from '../../components/EmailLogo';
import { EMAIL_STYLES } from '../../shared-styles';

interface TwoStepVerificationEmailProps {
Expand All @@ -14,12 +15,7 @@ export const TwoStepVerificationEmail = ({ validationCode, expMinutes }: TwoStep
<Preview>Verify your identity with Jetstream - {validationCode}</Preview>
<Body style={EMAIL_STYLES.main}>
<Container style={EMAIL_STYLES.container}>
<Img
src="https://res.cloudinary.com/getjetstream/image/upload/v1634516631/public/jetstream-logo-200w.png"
width="200"
alt="Jetstream"
style={EMAIL_STYLES.logo}
/>
<EmailLogo />
<Heading style={EMAIL_STYLES.codeTitle}>Verification code</Heading>

<Text style={EMAIL_STYLES.codeDescription}>
Expand Down
18 changes: 7 additions & 11 deletions libs/email/src/lib/email-templates/auth/VerifyEmail.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
import { Body, Button, Container, Head, Heading, Html, Img, Preview, Section, Text } from '@react-email/components';
import { Body, Button, Container, Head, Heading, Html, Preview, Section, Text } from '@react-email/components';
import * as React from 'react';
import { EmailFooter } from '../../components/EmailFooter';
import { EmailLogo } from '../../components/EmailLogo';
import { EMAIL_STYLES } from '../../shared-styles';

interface VerifyEmailProps {
baseUrl?: string;
validationCode: string;
expMinutes: number;
expHours: number;
}

export const VerifyEmail = ({ baseUrl = 'https://getjetstream.app', validationCode, expMinutes }: VerifyEmailProps) => (
export const VerifyEmail = ({ baseUrl = 'https://getjetstream.app', validationCode, expHours }: VerifyEmailProps) => (
<Html>
<Head />
<Preview>Verify your email address with Jetstream - {validationCode}</Preview>
<Body style={EMAIL_STYLES.main}>
<Container style={EMAIL_STYLES.container}>
<Img
src="https://res.cloudinary.com/getjetstream/image/upload/v1634516631/public/jetstream-logo-200w.png"
width="200"
alt="Jetstream"
style={EMAIL_STYLES.logo}
/>
<EmailLogo />
<Heading style={EMAIL_STYLES.codeTitle}>Verify your email address</Heading>

<Text style={EMAIL_STYLES.codeDescription}>
Enter this code in your open browser window or press the button below. This code will expire in {expMinutes} minutes.
Enter this code in your open browser window or press the button below. This code will expire in {expHours} hours.
</Text>

<Section style={EMAIL_STYLES.codeContainer}>
Expand All @@ -49,5 +45,5 @@ export default VerifyEmail;

VerifyEmail.PreviewProps = {
validationCode: '123456',
expMinutes: 10,
expHours: 48,
} as VerifyEmailProps;
37 changes: 6 additions & 31 deletions libs/email/src/lib/email-templates/auth/WelcomeEmail.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import { Body, Column, Container, Head, Heading, Hr, Html, Img, Link, Preview, Row, Section, Text } from '@react-email/components';
import * as React from 'react';
import { EmailFooter } from '../../components/EmailFooter';
import { EmailLogo } from '../../components/EmailLogo';
import { EMAIL_STYLES } from '../../shared-styles';

export const WelcomeEmail = () => (
<Html>
<Head />
<Preview>Welcome to Jetstream 🚀</Preview>
<Body style={main}>
<Container style={container}>
<Img
src="https://res.cloudinary.com/getjetstream/image/upload/v1634516631/public/jetstream-logo-200w.png"
width="200"
alt="Jetstream"
style={EMAIL_STYLES.logo}
/>
<Heading style={title}>We’re excited to welcome you to Jetstream!</Heading>
<Body style={EMAIL_STYLES.main}>
<Container style={EMAIL_STYLES.container}>
<EmailLogo />
<Heading style={EMAIL_STYLES.title}>We’re excited to welcome you to Jetstream!</Heading>

<Text style={description}>We’d love to hear from you! Share your thoughts on Jetstream.</Text>

<ul style={{ paddingLeft: '15px', fontSize: '14px' }}>
<ul style={{ paddingLeft: '15px', fontSize: '14px', listStyle: 'none' }}>
<li>
Send us an <Link href="mailto:[email protected]">email</Link>
</li>
Expand Down Expand Up @@ -165,27 +161,6 @@ function getFeatures() {
];
}

const main: React.CSSProperties = {
backgroundColor: '#ffffff',
fontFamily:
'-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol',
};

const container: React.CSSProperties = {
backgroundColor: '#ffffff',
border: '1px solid #ddd',
borderRadius: '5px',
marginTop: '20px',
width: '710px',
maxWidth: '100%',
margin: '0 auto',
padding: '5% 3%',
};

const title: React.CSSProperties = {
textAlign: 'center' as const,
};

const description: React.CSSProperties = {
textAlign: 'left' as const,
fontSize: 16,
Expand Down
Loading

0 comments on commit 134e28b

Please sign in to comment.