Skip to content

Commit bf3db78

Browse files
committed
Implement mfa emails and apply James' updates
1 parent f020c80 commit bf3db78

File tree

6 files changed

+177
-36
lines changed

6 files changed

+177
-36
lines changed

apps/webapp/app/routes/resources.account.mfa.setup/MfaDisableDialog.tsx

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ export function MfaDisableDialog({
6565
</DialogHeader>
6666
<Form method="post" onSubmit={handleSubmit}>
6767
{useRecoveryCode ? (
68-
<>
68+
<div className="pt-3">
6969
<Paragraph className="mb-6 text-center">
70-
Enter one of your recovery codes.
70+
Enter one of your recovery codes to disable MFA.
7171
</Paragraph>
7272
<Fieldset className="flex w-full flex-col items-center gap-y-2">
7373
<InputGroup>
@@ -84,28 +84,28 @@ export function MfaDisableDialog({
8484
/>
8585
</InputGroup>
8686
</Fieldset>
87-
88-
<Button
89-
type="button"
90-
onClick={handleSwitchToTotpCode}
91-
variant="minimal/small"
92-
className="mt-4"
93-
>
94-
Use an authenticator app
95-
</Button>
96-
</>
87+
<div className="flex justify-center">
88+
<Button
89+
type="button"
90+
onClick={handleSwitchToTotpCode}
91+
variant="minimal/small"
92+
className="my-4"
93+
>
94+
Use an authenticator app
95+
</Button>
96+
</div>
97+
</div>
9798
) : (
98-
<>
99+
<div className="pt-3">
99100
<Paragraph variant="base" className="mb-6 text-center">
100-
Enter the code from your authenticator app.
101+
Enter the code from your authenticator app to disable MFA.
101102
</Paragraph>
102103
<Fieldset className="flex w-full flex-col items-center gap-y-2">
103104
<InputOTP
104105
maxLength={6}
105106
value={totpCode}
106107
onChange={(value) => setTotpCode(value)}
107108
variant="large"
108-
fullWidth
109109
>
110110
<InputOTPGroup variant="large" fullWidth>
111111
<InputOTPSlot index={0} autoFocus variant="large" fullWidth />
@@ -117,15 +117,17 @@ export function MfaDisableDialog({
117117
</InputOTPGroup>
118118
</InputOTP>
119119
</Fieldset>
120-
<Button
121-
type="button"
122-
onClick={handleSwitchToRecoveryCode}
123-
variant="minimal/small"
124-
className="mt-4"
125-
>
126-
Use a recovery code
127-
</Button>
128-
</>
120+
<div className="flex justify-center">
121+
<Button
122+
type="button"
123+
onClick={handleSwitchToRecoveryCode}
124+
variant="minimal/small"
125+
className="my-4"
126+
>
127+
Use a recovery code
128+
</Button>
129+
</div>
130+
</div>
129131
)}
130132

131133
{error && <FormError>{error}</FormError>}
@@ -147,4 +149,4 @@ export function MfaDisableDialog({
147149
</DialogContent>
148150
</Dialog>
149151
);
150-
}
152+
}

apps/webapp/app/routes/resources.account.mfa.setup/MfaSetupDialog.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function MfaSetupDialog({
6060

6161
const downloadRecoveryCodes = () => {
6262
if (!recoveryCodes) return;
63-
63+
6464
const content = recoveryCodes.join("\n");
6565
const blob = new Blob([content], { type: "text/plain" });
6666
const url = URL.createObjectURL(blob);
@@ -87,12 +87,12 @@ export function MfaSetupDialog({
8787
Copy and store these recovery codes carefully in case you lose your device.
8888
</Paragraph>
8989

90-
<div className="flex flex-col gap-6 rounded border border-grid-dimmed bg-background-bright pt-6">
91-
<div className="grid grid-cols-3 gap-2">
90+
<div className="flex flex-col rounded border border-grid-dimmed bg-background-bright">
91+
<div className="grid grid-cols-3 gap-x-2 gap-y-4 px-3 py-6">
9292
{recoveryCodes.map((code, index) => (
93-
<div key={index} className="text-center font-mono text-sm text-text-bright">
93+
<span key={index} className="text-center font-mono text-xs text-text-bright">
9494
{code}
95-
</div>
95+
</span>
9696
))}
9797
</div>
9898
<div className="flex items-center justify-end border-t border-grid-bright px-1.5 py-1.5">
@@ -145,15 +145,15 @@ export function MfaSetupDialog({
145145
<div className="flex flex-col gap-4 pt-3">
146146
<Paragraph>
147147
Scan the QR code below with your preferred authenticator app then enter the 6 digit
148-
code that the app generates. Alternatively, you can copy the secret below and paste
149-
it into your app.
148+
code that the app generates. Alternatively, you can copy the secret below and paste it
149+
into your app.
150150
</Paragraph>
151151

152152
<div className="flex flex-col items-center justify-center gap-y-4 rounded border border-grid-dimmed bg-background-bright py-4">
153153
<div className="overflow-hidden rounded-lg border border-grid-dimmed">
154154
<QRCodeSVG value={setupData.otpAuthUrl} size={300} marginSize={3} />
155155
</div>
156-
<CopyableText value={setupData.secret} className="font-mono text-base tracking-wide" />
156+
<CopyableText value={setupData.secret} className="font-mono text-sm tracking-wide" />
157157
</div>
158158

159159
<div className="mb-4 flex items-center justify-center">
@@ -181,7 +181,7 @@ export function MfaSetupDialog({
181181
</div>
182182
</div>
183183

184-
{error && <FormError>{error}</FormError>}
184+
<div className="mb-4 flex justify-center">{error && <FormError>{error}</FormError>}</div>
185185

186186
<DialogFooter>
187187
<Button type="button" variant="secondary/medium" onClick={handleCancel}>
@@ -201,4 +201,4 @@ export function MfaSetupDialog({
201201
</DialogContent>
202202
</Dialog>
203203
);
204-
}
204+
}

apps/webapp/app/services/mfa/multiFactorAuthentication.server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createHash } from "@better-auth/utils/hash";
77
import { createOTP } from "@better-auth/utils/otp";
88
import { base32 } from "@better-auth/utils/base32";
99
import { z } from "zod";
10+
import { scheduleEmail } from "../email.server";
1011

1112
const generateRandomString = createRandomStringGenerator("A-Z", "0-9");
1213

@@ -99,6 +100,12 @@ export class MultiFactorAuthenticationService {
99100
},
100101
});
101102

103+
await scheduleEmail({
104+
email: "mfa-disabled",
105+
to: user.email,
106+
userEmail: user.email,
107+
});
108+
102109
return {
103110
success: true,
104111
};
@@ -226,6 +233,12 @@ export class MultiFactorAuthenticationService {
226233
});
227234
}
228235

236+
await scheduleEmail({
237+
email: "mfa-enabled",
238+
to: user.email,
239+
userEmail: user.email,
240+
});
241+
229242
return {
230243
success: true,
231244
recoveryCodes,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Body, Container, Head, Html, Preview, Text } from "@react-email/components";
2+
import { Footer } from "./components/Footer";
3+
import { Image } from "./components/Image";
4+
import { container, h1, main, paragraphLight } from "./components/styles";
5+
import { z } from "zod";
6+
7+
export const MfaDisabledEmailSchema = z.object({
8+
email: z.literal("mfa-disabled"),
9+
userEmail: z.string(),
10+
});
11+
12+
type MfaDisabledEmailProps = z.infer<typeof MfaDisabledEmailSchema>;
13+
14+
const previewDefaults: MfaDisabledEmailProps = {
15+
email: "mfa-disabled",
16+
userEmail: "user@example.com",
17+
};
18+
19+
export default function Email(props: MfaDisabledEmailProps) {
20+
const { userEmail } = {
21+
...previewDefaults,
22+
...props,
23+
};
24+
25+
return (
26+
<Html>
27+
<Head />
28+
<Preview>Multi-factor authentication disabled</Preview>
29+
<Body style={main}>
30+
<Container style={container}>
31+
<Text style={h1}>Multi-factor authentication disabled</Text>
32+
<Text style={paragraphLight}>Hi there,</Text>
33+
<Text style={paragraphLight}>
34+
You have successfully disabled multi-factor authentication (MFA) for your Trigger.dev
35+
account ({userEmail}). Your account no longer has the additional security layer provided
36+
by MFA.
37+
</Text>
38+
<Text style={paragraphLight}>
39+
You can re-enable MFA at any time from your account security page. If you didn't disable
40+
MFA, please contact our support team immediately.
41+
</Text>
42+
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
43+
<Footer />
44+
</Container>
45+
</Body>
46+
</Html>
47+
);
48+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { Body, Container, Head, Html, Preview, Text } from "@react-email/components";
2+
import { Footer } from "./components/Footer";
3+
import { Image } from "./components/Image";
4+
import { container, h1, main, paragraphLight } from "./components/styles";
5+
import { z } from "zod";
6+
7+
export const MfaEnabledEmailSchema = z.object({
8+
email: z.literal("mfa-enabled"),
9+
userEmail: z.string(),
10+
});
11+
12+
type MfaEnabledEmailProps = z.infer<typeof MfaEnabledEmailSchema>;
13+
14+
const previewDefaults: MfaEnabledEmailProps = {
15+
email: "mfa-enabled",
16+
userEmail: "user@example.com",
17+
};
18+
19+
export default function Email(props: MfaEnabledEmailProps) {
20+
const { userEmail } = {
21+
...previewDefaults,
22+
...props,
23+
};
24+
25+
return (
26+
<Html>
27+
<Head />
28+
<Preview>Multi-factor authentication enabled ✅</Preview>
29+
<Body style={main}>
30+
<Container style={container}>
31+
<Text style={h1}>Multi-factor authentication enabled</Text>
32+
<Text style={paragraphLight}>Hi there,</Text>
33+
<Text style={paragraphLight}>
34+
Multi-factor authentication was successfully enabled for your Trigger.dev account (
35+
{userEmail}). If you did not make this change, contact our support team immediately.
36+
</Text>
37+
<Text style={paragraphLight}>
38+
<strong>Staying secure:</strong>
39+
</Text>
40+
<Text style={paragraphLight}>
41+
• Keep your authenticator app safe and secured
42+
<br />
43+
• Never share your MFA codes with anyone
44+
<br />• Store your recovery codes in a secure location
45+
</Text>
46+
<Text
47+
style={{
48+
...paragraphLight,
49+
display: "block",
50+
marginBottom: "50px",
51+
}}
52+
>
53+
Your account now has an additional layer of protection and you'll need to enter a code
54+
from your authenticator app when logging in.
55+
</Text>
56+
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
57+
<Footer />
58+
</Container>
59+
</Body>
60+
</Html>
61+
);
62+
}

internal-packages/emails/src/index.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import AlertDeploymentSuccessEmail, {
1212
} from "../emails/deployment-success";
1313
import InviteEmail, { InviteEmailSchema } from "../emails/invite";
1414
import MagicLinkEmail from "../emails/magic-link";
15-
import WelcomeEmail from "../emails/welcome";
15+
1616
import { constructMailTransport, MailTransport, MailTransportOptions } from "./transports";
17+
import MfaEnabledEmail, { MfaEnabledEmailSchema } from "../emails/mfa-enabled";
18+
import MfaDisabledEmail, { MfaDisabledEmailSchema } from "../emails/mfa-disabled";
1719

1820
export { type MailTransportOptions };
1921

@@ -28,6 +30,8 @@ export const DeliverEmailSchema = z
2830
AlertAttemptEmailSchema,
2931
AlertDeploymentFailureEmailSchema,
3032
AlertDeploymentSuccessEmailSchema,
33+
MfaEnabledEmailSchema,
34+
MfaDisabledEmailSchema,
3135
])
3236
.and(z.object({ to: z.string() }));
3337

@@ -118,6 +122,18 @@ export class EmailClient {
118122
component: <AlertDeploymentSuccessEmail {...data} />,
119123
};
120124
}
125+
case "mfa-enabled": {
126+
return {
127+
subject: `Multi-factor authentication enabled on your Trigger.dev account`,
128+
component: <MfaEnabledEmail {...data} />,
129+
};
130+
}
131+
case "mfa-disabled": {
132+
return {
133+
subject: `Multi-factor authentication disabled on your Trigger.dev account`,
134+
component: <MfaDisabledEmail {...data} />,
135+
};
136+
}
121137
}
122138
}
123139
}

0 commit comments

Comments
 (0)