From 134e28b75f8be6084fa0184a7ea3c553da5813bb Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sun, 17 Nov 2024 10:38:15 -0700 Subject: [PATCH 1/5] Improve email templates 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 --- .../src/app/controllers/auth.controller.ts | 10 +++-- apps/landing/components/Footer.tsx | 3 +- .../components/auth/VerifyEmailOr2fa.tsx | 4 +- libs/api-config/src/lib/email.config.ts | 14 +++---- libs/auth/server/src/lib/auth.constants.ts | 1 + libs/email/src/lib/components/EmailFooter.tsx | 2 - libs/email/src/lib/components/EmailLogo.tsx | 14 +++++++ .../AuthenticationChangeConfirmationEmail.tsx | 10 ++--- .../lib/email-templates/auth/GenericEmail.tsx | 37 +++---------------- .../auth/PasswordResetConfirmationEmail.tsx | 10 ++--- .../auth/PasswordResetEmail.tsx | 10 ++--- .../auth/TwoStepVerificationEmail.tsx | 10 ++--- .../lib/email-templates/auth/VerifyEmail.tsx | 18 ++++----- .../lib/email-templates/auth/WelcomeEmail.tsx | 37 +++---------------- libs/email/src/lib/email.tsx | 4 +- libs/email/src/lib/shared-styles.ts | 9 ++++- 16 files changed, 73 insertions(+), 120 deletions(-) create mode 100644 libs/email/src/lib/components/EmailLogo.tsx diff --git a/apps/api/src/app/controllers/auth.controller.ts b/apps/api/src/app/controllers/auth.controller.ts index 70d22d51..0f56b422 100644 --- a/apps/api/src/app/controllers/auth.controller.ts +++ b/apps/api/src/app/controllers/auth.controller.ts @@ -5,6 +5,7 @@ import { createRememberDevice, createUserActivityFromReq, createUserActivityFromReqWithError, + EMAIL_VERIFICATION_TOKEN_DURATION_HOURS, ensureAuthError, ExpiredVerificationToken, generatePasswordResetToken, @@ -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'; @@ -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': @@ -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); } @@ -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': { diff --git a/apps/landing/components/Footer.tsx b/apps/landing/components/Footer.tsx index 62ab4f0e..83312131 100644 --- a/apps/landing/components/Footer.tsx +++ b/apps/landing/components/Footer.tsx @@ -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:support@getjetstream.app', target: '_blank' }, diff --git a/apps/landing/components/auth/VerifyEmailOr2fa.tsx b/apps/landing/components/auth/VerifyEmailOr2fa.tsx index 5db8013f..3a31c95b 100644 --- a/apps/landing/components/auth/VerifyEmailOr2fa.tsx +++ b/apps/landing/components/auth/VerifyEmailOr2fa.tsx @@ -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'; @@ -47,6 +48,7 @@ interface VerifyEmailOr2faProps { export function VerifyEmailOr2fa({ csrfToken, email, pendingVerifications }: VerifyEmailOr2faProps) { const router = useRouter(); + const searchParams = useSearchParams(); const [error, setError] = useState(); const [hasResent, setHasResent] = useState(false); @@ -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, diff --git a/libs/api-config/src/lib/email.config.ts b/libs/api-config/src/lib/email.config.ts index 61371b1f..962cd665 100644 --- a/libs/api-config/src/lib/email.config.ts +++ b/libs/api-config/src/lib/email.config.ts @@ -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')); } diff --git a/libs/auth/server/src/lib/auth.constants.ts b/libs/auth/server/src/lib/auth.constants.ts index 7212f278..35c95f61 100644 --- a/libs/auth/server/src/lib/auth.constants.ts +++ b/libs/auth/server/src/lib/auth.constants.ts @@ -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; diff --git a/libs/email/src/lib/components/EmailFooter.tsx b/libs/email/src/lib/components/EmailFooter.tsx index c098600a..86db0e53 100644 --- a/libs/email/src/lib/components/EmailFooter.tsx +++ b/libs/email/src/lib/components/EmailFooter.tsx @@ -18,7 +18,6 @@ export const EmailFooter = () => { fontSize: 14, lineHeight: '18px', fontWeight: 600, - color: 'rgb(17,24,39)', textTransform: 'uppercase', }} > @@ -49,7 +48,6 @@ export const EmailFooter = () => { lineHeight: '18px', fontWeight: 600, color: 'rgb(107,114,128)', - textTransform: 'uppercase', }} > support@getjetstream.app diff --git a/libs/email/src/lib/components/EmailLogo.tsx b/libs/email/src/lib/components/EmailLogo.tsx new file mode 100644 index 00000000..71496f74 --- /dev/null +++ b/libs/email/src/lib/components/EmailLogo.tsx @@ -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 ( + Jetstream logo + ); +}; diff --git a/libs/email/src/lib/email-templates/auth/AuthenticationChangeConfirmationEmail.tsx b/libs/email/src/lib/email-templates/auth/AuthenticationChangeConfirmationEmail.tsx index 95505109..69237f7a 100644 --- a/libs/email/src/lib/email-templates/auth/AuthenticationChangeConfirmationEmail.tsx +++ b/libs/email/src/lib/email-templates/auth/AuthenticationChangeConfirmationEmail.tsx @@ -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 { @@ -20,12 +21,7 @@ export const AuthenticationChangeConfirmationEmail = ({ {preview} - Jetstream + {heading} {!!additionalTextSegments?.length && ( diff --git a/libs/email/src/lib/email-templates/auth/GenericEmail.tsx b/libs/email/src/lib/email-templates/auth/GenericEmail.tsx index 31f96953..c26d38dc 100644 --- a/libs/email/src/lib/email-templates/auth/GenericEmail.tsx +++ b/libs/email/src/lib/email-templates/auth/GenericEmail.tsx @@ -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 { @@ -13,15 +14,10 @@ export const GenericEmail = ({ preview, heading, segments }: GenericEmailProps) {preview} - - - Jetstream - {heading} + + + + {heading}
{segments.map((text, index) => ( @@ -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, diff --git a/libs/email/src/lib/email-templates/auth/PasswordResetConfirmationEmail.tsx b/libs/email/src/lib/email-templates/auth/PasswordResetConfirmationEmail.tsx index f52e10a4..e726b990 100644 --- a/libs/email/src/lib/email-templates/auth/PasswordResetConfirmationEmail.tsx +++ b/libs/email/src/lib/email-templates/auth/PasswordResetConfirmationEmail.tsx @@ -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 = () => { @@ -10,12 +11,7 @@ export const PasswordResetConfirmationEmail = () => { Your password has been reset - Jetstream + Your password has been successfully reset Didn't request this? diff --git a/libs/email/src/lib/email-templates/auth/PasswordResetEmail.tsx b/libs/email/src/lib/email-templates/auth/PasswordResetEmail.tsx index 42c73cbf..83b313ab 100644 --- a/libs/email/src/lib/email-templates/auth/PasswordResetEmail.tsx +++ b/libs/email/src/lib/email-templates/auth/PasswordResetEmail.tsx @@ -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 { @@ -24,12 +25,7 @@ export const PasswordResetEmail = ({ Reset your password with Jetstream - Jetstream + Reset your password diff --git a/libs/email/src/lib/email-templates/auth/TwoStepVerificationEmail.tsx b/libs/email/src/lib/email-templates/auth/TwoStepVerificationEmail.tsx index 26ddc3f7..c6043d08 100644 --- a/libs/email/src/lib/email-templates/auth/TwoStepVerificationEmail.tsx +++ b/libs/email/src/lib/email-templates/auth/TwoStepVerificationEmail.tsx @@ -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 { @@ -14,12 +15,7 @@ export const TwoStepVerificationEmail = ({ validationCode, expMinutes }: TwoStep Verify your identity with Jetstream - {validationCode} - Jetstream + Verification code diff --git a/libs/email/src/lib/email-templates/auth/VerifyEmail.tsx b/libs/email/src/lib/email-templates/auth/VerifyEmail.tsx index 811fba78..658da1df 100644 --- a/libs/email/src/lib/email-templates/auth/VerifyEmail.tsx +++ b/libs/email/src/lib/email-templates/auth/VerifyEmail.tsx @@ -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) => ( Verify your email address with Jetstream - {validationCode} - Jetstream + Verify your email address - 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.
@@ -49,5 +45,5 @@ export default VerifyEmail; VerifyEmail.PreviewProps = { validationCode: '123456', - expMinutes: 10, + expHours: 48, } as VerifyEmailProps; diff --git a/libs/email/src/lib/email-templates/auth/WelcomeEmail.tsx b/libs/email/src/lib/email-templates/auth/WelcomeEmail.tsx index bf05c13a..25db57a3 100644 --- a/libs/email/src/lib/email-templates/auth/WelcomeEmail.tsx +++ b/libs/email/src/lib/email-templates/auth/WelcomeEmail.tsx @@ -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 = () => ( Welcome to Jetstream 🚀 - - - Jetstream - We’re excited to welcome you to Jetstream! + + + + We’re excited to welcome you to Jetstream! We’d love to hear from you! Share your thoughts on Jetstream. -
    +
    • Send us an email
    • @@ -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, diff --git a/libs/email/src/lib/email.tsx b/libs/email/src/lib/email.tsx index dbb7496c..90ac64ed 100644 --- a/libs/email/src/lib/email.tsx +++ b/libs/email/src/lib/email.tsx @@ -101,8 +101,8 @@ export async function sendInternalAccountDeletionEmail(userId: string, reason?: }); } -export async function sendEmailVerification(emailAddress: string, code: string, expMinutes: number) { - const component = ; +export async function sendEmailVerification(emailAddress: string, code: string, expHours: number) { + const component = ; const [html, text] = await renderComponent(component); await sendEmail({ diff --git a/libs/email/src/lib/shared-styles.ts b/libs/email/src/lib/shared-styles.ts index 62505787..6f40a328 100644 --- a/libs/email/src/lib/shared-styles.ts +++ b/libs/email/src/lib/shared-styles.ts @@ -1,6 +1,6 @@ const main: React.CSSProperties = { backgroundColor: '#ffffff', - // fontFamily: 'HelveticaNeue,Helvetica,Arial,sans-serif', + color: '#111827', fontFamily: '-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol', textAlign: 'center' as const, @@ -21,12 +21,18 @@ const container: React.CSSProperties = { padding: '5% 3%', }; +const title: React.CSSProperties = { + textAlign: 'center' as const, +}; + const codeTitle: React.CSSProperties = { textAlign: 'center' as const, + color: '#111827', }; const codeDescription: React.CSSProperties = { textAlign: 'center' as const, + color: '#444', }; const codeContainer: React.CSSProperties = { @@ -90,6 +96,7 @@ export const EMAIL_STYLES = { main, logo, container, + title, codeTitle, codeDescription, codeContainer, From d86dd586d7f8fd576479eb4806c8e2919ae8f7db Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sun, 17 Nov 2024 10:43:47 -0700 Subject: [PATCH 2/5] Fix version Ensure version is included in environment, this appears to have been omitted when we migrated to ZOD Work towards #1076 --- apps/api/src/app/routes/api.routes.ts | 2 +- apps/api/src/main.ts | 2 +- apps/cron-tasks/src/config/env-config.ts | 2 +- libs/api-config/src/lib/api-rollbar-config.ts | 4 ++-- libs/api-config/src/lib/env-config.ts | 7 +++---- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/api/src/app/routes/api.routes.ts b/apps/api/src/app/routes/api.routes.ts index cfc9eb27..f0401cb1 100644 --- a/apps/api/src/app/routes/api.routes.ts +++ b/apps/api/src/app/routes/api.routes.ts @@ -24,7 +24,7 @@ routes.use(addOrgsToLocal); // used to make sure the user is authenticated and can communicate with the server routes.get('/heartbeat', (req: express.Request, res: express.Response) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - sendJson(res as any, { version: ENV.GIT_VERSION || null, announcements: getAnnouncements() }); + sendJson(res as any, { version: ENV.VERSION || null, announcements: getAnnouncements() }); }); /** diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index c96a4bbf..68d90a25 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -53,7 +53,7 @@ if (cluster.isPrimary) { NODE_ENV=${ENV.NODE_ENV} ENVIRONMENT=${ENV.ENVIRONMENT} -GIT_VERSION=${ENV.GIT_VERSION ?? ''} +VERSION=${ENV.VERSION ?? ''} LOG_LEVEL=${ENV.LOG_LEVEL} JETSTREAM_SERVER_URL=${ENV.JETSTREAM_SERVER_URL} JETSTREAM_CLIENT_URL=${ENV.JETSTREAM_CLIENT_URL} diff --git a/apps/cron-tasks/src/config/env-config.ts b/apps/cron-tasks/src/config/env-config.ts index 0ffc6f91..673e7e98 100644 --- a/apps/cron-tasks/src/config/env-config.ts +++ b/apps/cron-tasks/src/config/env-config.ts @@ -8,7 +8,7 @@ dotenv.config(); let VERSION; try { VERSION = readFileSync(join(__dirname, '../../VERSION'), 'utf-8').trim(); - console.warn(`APP VERSION ${VERSION}`); + console.info(`APP VERSION ${VERSION}`); } catch (ex) { console.warn('COULD NOT READ VERSION FILE', getExceptionLog(ex)); } diff --git a/libs/api-config/src/lib/api-rollbar-config.ts b/libs/api-config/src/lib/api-rollbar-config.ts index 0420c0f9..573ef2af 100644 --- a/libs/api-config/src/lib/api-rollbar-config.ts +++ b/libs/api-config/src/lib/api-rollbar-config.ts @@ -2,8 +2,8 @@ import Rollbar from 'rollbar'; import { ENV } from './env-config'; export const rollbarServer = new Rollbar({ - codeVersion: ENV.GIT_VERSION || '', - code_version: ENV.GIT_VERSION || '', + codeVersion: ENV.VERSION || '', + code_version: ENV.VERSION || '', accessToken: ENV.ROLLBAR_SERVER_TOKEN || '', environment: ENV.ENVIRONMENT, captureUncaught: true, diff --git a/libs/api-config/src/lib/env-config.ts b/libs/api-config/src/lib/env-config.ts index 4d7de9d2..fd419e3e 100644 --- a/libs/api-config/src/lib/env-config.ts +++ b/libs/api-config/src/lib/env-config.ts @@ -7,16 +7,14 @@ import { readFileSync } from 'fs-extra'; import { isNumber } from 'lodash'; import { join } from 'path'; import { z } from 'zod'; -import { getExceptionLog } from './api-logger'; dotenv.config(); let VERSION = 'unknown'; try { VERSION = readFileSync(join(__dirname, '../../VERSION'), 'utf-8').trim(); - console.warn(`APP VERSION ${VERSION}`); } catch (ex) { - console.warn('COULD NOT READ VERSION FILE', getExceptionLog(ex)); + // ignore errors } /** @@ -103,7 +101,7 @@ const envSchema = z.object({ CAPTCHA_SECRET_KEY: z.string().optional(), CAPTCHA_PROPERTY: z.literal('captchaToken').optional().default('captchaToken'), IP_API_KEY: z.string().optional().describe('API Key used to get location information from IP address'), - GIT_VERSION: z.string().optional(), + VERSION: z.string().optional(), ROLLBAR_SERVER_TOKEN: z.string().optional(), // Legacy Auth0 - Used to allow JIT password migration @@ -202,6 +200,7 @@ const parseResults = envSchema.safeParse({ EXAMPLE_USER_PASSWORD: ensureBoolean(process.env.EXAMPLE_USER_OVERRIDE) ? process.env.EXAMPLE_USER_PASSWORD : null, EXAMPLE_USER_FULL_PROFILE: ensureBoolean(process.env.EXAMPLE_USER_OVERRIDE) ? EXAMPLE_USER_FULL_PROFILE : null, SFDC_API_VERSION: process.env.NX_SFDC_API_VERSION || process.env.SFDC_API_VERSION, + VERSION, }); if (!parseResults.success) { From af8a592ffbaa68422a8994fb2014f306e0e7cb83 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sun, 17 Nov 2024 11:09:59 -0700 Subject: [PATCH 3/5] Ensure turnstile is reset on form submission error Work towards #1076 --- apps/landing/components/auth/Captcha.tsx | 15 +++++++++++---- apps/landing/components/auth/LoginOrSignUp.tsx | 10 +++++++++- .../landing/components/auth/PasswordResetInit.tsx | 10 +++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/apps/landing/components/auth/Captcha.tsx b/apps/landing/components/auth/Captcha.tsx index 0b28fa97..0dc6c4a7 100644 --- a/apps/landing/components/auth/Captcha.tsx +++ b/apps/landing/components/auth/Captcha.tsx @@ -1,5 +1,6 @@ -import { Turnstile } from '@marsidev/react-turnstile'; -import { useId } from 'react'; +import { Maybe } from '@jetstream/types'; +import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile'; +import { forwardRef, useId, useImperativeHandle, useRef } from 'react'; import { ENVIRONMENT } from '../../utils/environment'; interface CaptchaProps { @@ -17,9 +18,13 @@ interface CaptchaProps { onFinished: () => void; } -export function Captcha({ action, formError, onLoad, onChange, onFinished }: CaptchaProps) { +// eslint-disable-next-line react/display-name +export const Captcha = forwardRef, CaptchaProps>(({ action, formError, onLoad, onChange, onFinished }, ref) => { + const turnstileRef = useRef(null); const id = useId(); + useImperativeHandle>(ref, () => turnstileRef.current, [turnstileRef]); + // Skip rendering the captcha if we're running in Playwright or if the key is not set // In real environments the server will still validate and prevent access if there isn't a valid token if (!ENVIRONMENT.CAPTCHA_KEY || (window as any)?.playwright) { @@ -31,6 +36,7 @@ export function Captcha({ action, formError, onLoad, onChange, onFinished }: Cap <> { @@ -52,4 +59,4 @@ export function Captcha({ action, formError, onLoad, onChange, onFinished }: Cap )} ); -} +}); diff --git a/apps/landing/components/auth/LoginOrSignUp.tsx b/apps/landing/components/auth/LoginOrSignUp.tsx index b4452e24..b5b62ab7 100644 --- a/apps/landing/components/auth/LoginOrSignUp.tsx +++ b/apps/landing/components/auth/LoginOrSignUp.tsx @@ -1,9 +1,10 @@ /* eslint-disable @next/next/no-img-element */ import { zodResolver } from '@hookform/resolvers/zod'; import { Providers } from '@jetstream/auth/types'; +import { TurnstileInstance } from '@marsidev/react-turnstile'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { Fragment, useState } from 'react'; +import { Fragment, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { AUTH_PATHS, ENVIRONMENT } from '../../utils/environment'; @@ -53,6 +54,7 @@ export function LoginOrSignUp({ action, providers, csrfToken }: LoginOrSignUpPro const router = useRouter(); const [showPasswordActive, setShowPasswordActive] = useState(false); const [finishedCaptcha, setFinishedCaptcha] = useState(false); + const captchaRef = useRef(null); const { register, @@ -100,6 +102,11 @@ export function LoginOrSignUp({ action, providers, csrfToken }: LoginOrSignUpPro if (!response.ok || error) { router.push(`${router.pathname}?${new URLSearchParams({ error: errorType || 'UNKNOWN_ERROR' })}`); + try { + captchaRef?.current?.reset(); + } catch (ex) { + console.error('Error resetting captcha', ex); + } return; } @@ -258,6 +265,7 @@ export function LoginOrSignUp({ action, providers, csrfToken }: LoginOrSignUpPro setValue('captchaToken', token)} diff --git a/apps/landing/components/auth/PasswordResetInit.tsx b/apps/landing/components/auth/PasswordResetInit.tsx index b158a4b5..1c9de435 100644 --- a/apps/landing/components/auth/PasswordResetInit.tsx +++ b/apps/landing/components/auth/PasswordResetInit.tsx @@ -1,8 +1,9 @@ /* eslint-disable @next/next/no-img-element */ import { zodResolver } from '@hookform/resolvers/zod'; +import { TurnstileInstance } from '@marsidev/react-turnstile'; import Link from 'next/link'; import { useRouter } from 'next/router'; -import { Fragment, useState } from 'react'; +import { Fragment, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { AUTH_PATHS } from '../../utils/environment'; @@ -29,6 +30,7 @@ export function PasswordResetInit({ csrfToken }: PasswordResetInitProps) { const [isSubmitted, setIsSubmitted] = useState(false); const [finishedCaptcha, setFinishedCaptcha] = useState(false); const [error, setError] = useState(); + const captchaRef = useRef(null); const { register, @@ -58,6 +60,11 @@ export function PasswordResetInit({ csrfToken }: PasswordResetInitProps) { }); if (!response.ok) { + try { + captchaRef?.current?.reset(); + } catch (ex) { + console.error('Error resetting captcha', ex); + } throw new Error('Unable to initialize the reset process'); } @@ -127,6 +134,7 @@ export function PasswordResetInit({ csrfToken }: PasswordResetInitProps) { /> setValue('captchaToken', token)} From a5ae6d9b9da04dc268916a96c8713f46034bf360 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sun, 17 Nov 2024 11:20:57 -0700 Subject: [PATCH 4/5] remove deletedAt field from user table Since users are now hard-deleted, we no longer need this field Work towards #1076 --- apps/api/src/app/db/transactions.db.ts | 24 ------- apps/api/src/main.ts | 1 - apps/cron-tasks/src/utils/cron-utils.ts | 29 -------- package.json | 4 +- prisma/schema.prisma | 1 - yarn.lock | 90 ++++++++++++------------- 6 files changed, 47 insertions(+), 102 deletions(-) delete mode 100644 apps/cron-tasks/src/utils/cron-utils.ts diff --git a/apps/api/src/app/db/transactions.db.ts b/apps/api/src/app/db/transactions.db.ts index da528142..0d9e17b8 100644 --- a/apps/api/src/app/db/transactions.db.ts +++ b/apps/api/src/app/db/transactions.db.ts @@ -1,34 +1,10 @@ import { getExceptionLog, logger, prisma } from '@jetstream/api-config'; -import { UserProfileSession } from '@jetstream/auth/types'; import { PrismaPromise } from '@prisma/client'; /** * This file manages db operations as transactions that span multiple tables */ -export async function deleteUserAndOrgs(user: UserProfileSession) { - if (!user?.id) { - throw new Error('A valid user must be provided'); - } - try { - const deleteOrgs = prisma.salesforceOrg.deleteMany({ - where: { jetstreamUserId: user.id }, - }); - - const deleteUser = prisma.user.update({ - where: { userId: user.id }, - data: { - deletedAt: new Date(), - }, - }); - - await prisma.$transaction([deleteOrgs, deleteUser]); - } catch (ex) { - logger.error({ userId: user?.id, ...getExceptionLog(ex) }, '[DB][TX][DEL_ORGS_AND_USER][ERROR] %o', ex); - throw ex; - } -} - /** * Hard delete all orgs and users for a given org. * If no user exists or no orgs exist, then no db transaction is submitted diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 68d90a25..8b6b06a9 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -350,7 +350,6 @@ try { name: user.name, password: passwordHash, passwordUpdatedAt: new Date(), - deletedAt: null, lastLoggedIn: new Date(), preferences: { create: { skipFrontdoorLogin: false } }, authFactors: { create: { type: '2fa-email', enabled: false } }, diff --git a/apps/cron-tasks/src/utils/cron-utils.ts b/apps/cron-tasks/src/utils/cron-utils.ts deleted file mode 100644 index 77e6fff1..00000000 --- a/apps/cron-tasks/src/utils/cron-utils.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { prisma } from '../config/db.config'; -import { logger } from '../config/logger.config'; - -export async function deleteUserAndOrgs(user: any) { - if (!user?.user_id) { - throw new Error('A valid user must be provided'); - } - try { - const deleteOrgs = prisma.salesforceOrg.deleteMany({ - where: { jetstreamUserId: user.user_id }, - }); - - const deleteUser = prisma.user.update({ - where: { userId: user.user_id }, - data: { - deletedAt: new Date(), - }, - }); - - const results = await prisma.$transaction([deleteOrgs, deleteUser]); - return { - orgCount: results[0].count, - userId: results[1].id, - }; - } catch (ex) { - logger.error({ message: ex.message, stack: ex.stack }, '[DB][TX][DEL_ORGS_AND_USER][ERROR]'); - throw ex; - } -} diff --git a/package.json b/package.json index 1ee1dcd3..0974d2d3 100644 --- a/package.json +++ b/package.json @@ -204,7 +204,7 @@ "postcss": "^8.4.48", "postcss-preset-env": "7", "prettier": "2.7.0", - "prisma": "^5.21.1", + "prisma": "^5.22.0", "react-email": "3.0.1", "react-refresh": "^0.10.0", "release-it": "^17.10.0", @@ -253,7 +253,7 @@ "@oslojs/otp": "^1.0.0", "@panva/hkdf": "^1.2.1", "@popperjs/core": "^2.11.8", - "@prisma/client": "^5.21.1", + "@prisma/client": "^5.22.0", "@react-aria/dialog": "^3.5.12", "@react-aria/focus": "^3.16.2", "@react-aria/overlays": "^3.21.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1579c004..8b8fe8bf 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -30,7 +30,6 @@ model User { rememberdDevices RememberedDevice[] passwordResetTokens PasswordResetToken[] lastLoggedIn DateTime? - deletedAt DateTime? createdAt DateTime @default(now()) @db.Timestamp(6) updatedAt DateTime @updatedAt } diff --git a/yarn.lock b/yarn.lock index 5562b8fd..2b0afaef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7291,46 +7291,46 @@ resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz" integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw== -"@prisma/client@^5.21.1": - version "5.21.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.21.1.tgz#ad51ef220eb80173f882e859960d81e626b73898" - integrity sha512-3n+GgbAZYjaS/k0M03yQsQfR1APbr411r74foknnsGpmhNKBG49VuUkxIU6jORgvJPChoD4WC4PqoHImN1FP0w== - -"@prisma/debug@5.21.1": - version "5.21.1" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.21.1.tgz#df4383cb8a6273b1d6112cda0f1d5bef73e71be7" - integrity sha512-uY8SAhcnORhvgtOrNdvWS98Aq/nkQ9QDUxrWAgW8XrCZaI3j2X7zb7Xe6GQSh6xSesKffFbFlkw0c2luHQviZA== - -"@prisma/engines-version@5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36": - version "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36.tgz#8a5f136a8ee71995bf635686bd2f1a6650f9320c" - integrity sha512-qvnEflL0//lh44S/T9NcvTMxfyowNeUxTunPcDfKPjyJNrCNf2F1zQLcUv5UHAruECpX+zz21CzsC7V2xAeM7Q== - -"@prisma/engines@5.21.1": - version "5.21.1" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.21.1.tgz#05f9bc50eb4aa169b31cadfb402165bd44e0653f" - integrity sha512-hGVTldUkIkTwoV8//hmnAAiAchi4oMEKD3aW5H2RrnI50tTdwza7VQbTTAyN3OIHWlK5DVg6xV7X8N/9dtOydA== - dependencies: - "@prisma/debug" "5.21.1" - "@prisma/engines-version" "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36" - "@prisma/fetch-engine" "5.21.1" - "@prisma/get-platform" "5.21.1" - -"@prisma/fetch-engine@5.21.1": - version "5.21.1" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.21.1.tgz#c56008f954199a3f3f2183d892f093f64976e4d8" - integrity sha512-70S31vgpCGcp9J+mh/wHtLCkVezLUqe/fGWk3J3JWZIN7prdYSlr1C0niaWUyNK2VflLXYi8kMjAmSxUVq6WGQ== - dependencies: - "@prisma/debug" "5.21.1" - "@prisma/engines-version" "5.21.1-1.bf0e5e8a04cada8225617067eaa03d041e2bba36" - "@prisma/get-platform" "5.21.1" - -"@prisma/get-platform@5.21.1": - version "5.21.1" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.21.1.tgz#a2219e7755cec881dffc66469c31bb0975a95b54" - integrity sha512-sRxjL3Igst3ct+e8ya/x//cDXmpLbZQ5vfps2N4tWl4VGKQAmym77C/IG/psSMsQKszc8uFC/q1dgmKFLUgXZQ== - dependencies: - "@prisma/debug" "5.21.1" +"@prisma/client@^5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.22.0.tgz#da1ca9c133fbefe89e0da781c75e1c59da5f8802" + integrity sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA== + +"@prisma/debug@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.22.0.tgz#58af56ed7f6f313df9fb1042b6224d3174bbf412" + integrity sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ== + +"@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2": + version "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz#d534dd7235c1ba5a23bacd5b92cc0ca3894c28f4" + integrity sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ== + +"@prisma/engines@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.22.0.tgz#28f3f52a2812c990a8b66eb93a0987816a5b6d84" + integrity sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA== + dependencies: + "@prisma/debug" "5.22.0" + "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" + "@prisma/fetch-engine" "5.22.0" + "@prisma/get-platform" "5.22.0" + +"@prisma/fetch-engine@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz#4fb691b483a450c5548aac2f837b267dd50ef52e" + integrity sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA== + dependencies: + "@prisma/debug" "5.22.0" + "@prisma/engines-version" "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2" + "@prisma/get-platform" "5.22.0" + +"@prisma/get-platform@5.22.0": + version "5.22.0" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.22.0.tgz#fc675bc9d12614ca2dade0506c9c4a77e7dddacd" + integrity sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q== + dependencies: + "@prisma/debug" "5.22.0" "@prisma/prisma-fmt-wasm@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" @@ -21400,12 +21400,12 @@ pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" -prisma@^5.21.1: - version "5.21.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.21.1.tgz#3ffe4f4b60ea8df2e6d5f24f0cea090bcc5c0bd6" - integrity sha512-PB+Iqzld/uQBPaaw2UVIk84kb0ITsLajzsxzsadxxl54eaU5Gyl2/L02ysivHxK89t7YrfQJm+Ggk37uvM70oQ== +prisma@^5.22.0: + version "5.22.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.22.0.tgz#1f6717ff487cdef5f5799cc1010459920e2e6197" + integrity sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A== dependencies: - "@prisma/engines" "5.21.1" + "@prisma/engines" "5.22.0" optionalDependencies: fsevents "2.3.3" From ebed60a0afb76ab40f793d0bb2c65ddee383277f Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Sun, 17 Nov 2024 11:42:57 -0700 Subject: [PATCH 5/5] Add link in email for 2FA In addition to entering code, users can click the link to open jetstream and auto-enter the code work towards #1076 --- libs/email/src/lib/components/EmailFooter.tsx | 11 ++++++----- .../auth/TwoStepVerificationEmail.tsx | 15 +++++++++++++-- .../src/lib/email-templates/auth/VerifyEmail.tsx | 2 +- libs/email/src/lib/email.tsx | 2 +- libs/email/src/lib/shared-styles.ts | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/libs/email/src/lib/components/EmailFooter.tsx b/libs/email/src/lib/components/EmailFooter.tsx index 86db0e53..6160be22 100644 --- a/libs/email/src/lib/components/EmailFooter.tsx +++ b/libs/email/src/lib/components/EmailFooter.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Link, Section, Text } from '@react-email/components'; import * as React from 'react'; export const EmailFooter = () => { @@ -34,24 +34,25 @@ export const EmailFooter = () => { fontSize: 12, lineHeight: '18px', fontWeight: 600, - color: 'rgb(107,114,128)', + color: '#6b7280', textTransform: 'uppercase', }} > Whitefish, MT USA - support@getjetstream.app - + diff --git a/libs/email/src/lib/email-templates/auth/TwoStepVerificationEmail.tsx b/libs/email/src/lib/email-templates/auth/TwoStepVerificationEmail.tsx index c6043d08..f38a3b24 100644 --- a/libs/email/src/lib/email-templates/auth/TwoStepVerificationEmail.tsx +++ b/libs/email/src/lib/email-templates/auth/TwoStepVerificationEmail.tsx @@ -1,15 +1,20 @@ -import { Body, Container, Head, Heading, Html, Preview, Section, Text } from '@react-email/components'; +import { Body, 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 TwoStepVerificationEmailProps { + baseUrl: string; validationCode: string; expMinutes: number; } -export const TwoStepVerificationEmail = ({ validationCode, expMinutes }: TwoStepVerificationEmailProps) => ( +export const TwoStepVerificationEmail = ({ + baseUrl = 'https://getjetstream.app', + validationCode, + expMinutes, +}: TwoStepVerificationEmailProps) => ( Verify your identity with Jetstream - {validationCode} @@ -26,6 +31,12 @@ export const TwoStepVerificationEmail = ({ validationCode, expMinutes }: TwoStep {validationCode}
+
+ + Or click this link + +
+ Didn't request this? If you didn't make this request, you can safely ignore this email.
diff --git a/libs/email/src/lib/email-templates/auth/VerifyEmail.tsx b/libs/email/src/lib/email-templates/auth/VerifyEmail.tsx index 658da1df..c478005f 100644 --- a/libs/email/src/lib/email-templates/auth/VerifyEmail.tsx +++ b/libs/email/src/lib/email-templates/auth/VerifyEmail.tsx @@ -5,7 +5,7 @@ import { EmailLogo } from '../../components/EmailLogo'; import { EMAIL_STYLES } from '../../shared-styles'; interface VerifyEmailProps { - baseUrl?: string; + baseUrl: string; validationCode: string; expHours: number; } diff --git a/libs/email/src/lib/email.tsx b/libs/email/src/lib/email.tsx index 90ac64ed..78924809 100644 --- a/libs/email/src/lib/email.tsx +++ b/libs/email/src/lib/email.tsx @@ -114,7 +114,7 @@ export async function sendEmailVerification(emailAddress: string, code: string, } export async function sendVerificationCode(emailAddress: string, code: string, expMinutes: number) { - const component = ; + const component = ; const [html, text] = await renderComponent(component); await sendEmail({ diff --git a/libs/email/src/lib/shared-styles.ts b/libs/email/src/lib/shared-styles.ts index 6f40a328..cc873f57 100644 --- a/libs/email/src/lib/shared-styles.ts +++ b/libs/email/src/lib/shared-styles.ts @@ -88,7 +88,7 @@ const paragraph: React.CSSProperties = { }; const link: React.CSSProperties = { - color: '#444', + color: '#009aff', textDecoration: 'underline', };