Skip to content

Commit c0330ac

Browse files
Add two-factor authentication (#692)
Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
1 parent a8bc176 commit c0330ac

21 files changed

+1177
-152
lines changed

components/Settings.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ export default function SettingsShell(props) {
1414
current: router.pathname == "/settings/profile",
1515
},
1616
{
17-
name: "Password",
18-
href: "/settings/password",
17+
name: "Security",
18+
href: "/settings/security",
1919
icon: KeyIcon,
20-
current: router.pathname == "/settings/password",
20+
current: router.pathname == "/settings/security",
2121
},
2222
{ name: "Embed", href: "/settings/embed", icon: CodeIcon, current: router.pathname == "/settings/embed" },
2323
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React, { SyntheticEvent, useState } from "react";
2+
import Modal from "@components/Modal";
3+
import { ErrorCode } from "@lib/auth";
4+
5+
const errorMessages: { [key: string]: string } = {
6+
[ErrorCode.IncorrectPassword]: "Current password is incorrect",
7+
[ErrorCode.NewPasswordMatchesOld]:
8+
"New password matches your old password. Please choose a different password.",
9+
};
10+
11+
const ChangePasswordSection = () => {
12+
const [oldPassword, setOldPassword] = useState("");
13+
const [newPassword, setNewPassword] = useState("");
14+
const [successModalOpen, setSuccessModalOpen] = useState(false);
15+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
16+
const [isSubmitting, setIsSubmitting] = useState(false);
17+
18+
const closeSuccessModal = () => {
19+
setSuccessModalOpen(false);
20+
};
21+
22+
async function changePasswordHandler(e: SyntheticEvent) {
23+
e.preventDefault();
24+
25+
if (isSubmitting) {
26+
return;
27+
}
28+
29+
setIsSubmitting(true);
30+
setErrorMessage(null);
31+
32+
try {
33+
const response = await fetch("/api/auth/changepw", {
34+
method: "PATCH",
35+
body: JSON.stringify({ oldPassword, newPassword }),
36+
headers: {
37+
"Content-Type": "application/json",
38+
},
39+
});
40+
41+
if (response.status === 200) {
42+
setOldPassword("");
43+
setNewPassword("");
44+
setSuccessModalOpen(true);
45+
return;
46+
}
47+
48+
const body = await response.json();
49+
setErrorMessage(errorMessages[body.error] || "Something went wrong. Please try again");
50+
} catch (err) {
51+
console.error("Error changing password", err);
52+
setErrorMessage("Something went wrong. Please try again");
53+
} finally {
54+
setIsSubmitting(false);
55+
}
56+
}
57+
58+
return (
59+
<>
60+
<div className="mt-6">
61+
<h2 className="text-lg leading-6 font-medium text-gray-900">Change Password</h2>
62+
</div>
63+
<form className="divide-y divide-gray-200 lg:col-span-9" onSubmit={changePasswordHandler}>
64+
<div className="py-6 lg:pb-8">
65+
<div className="flex">
66+
<div className="w-1/2 mr-2">
67+
<label htmlFor="current_password" className="block text-sm font-medium text-gray-700">
68+
Current Password
69+
</label>
70+
<div className="mt-1">
71+
<input
72+
type="password"
73+
value={oldPassword}
74+
onInput={(e) => setOldPassword(e.currentTarget.value)}
75+
name="current_password"
76+
id="current_password"
77+
required
78+
className="shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
79+
placeholder="Your old password"
80+
/>
81+
</div>
82+
</div>
83+
<div className="w-1/2 ml-2">
84+
<label htmlFor="new_password" className="block text-sm font-medium text-gray-700">
85+
New Password
86+
</label>
87+
<div className="mt-1">
88+
<input
89+
type="password"
90+
name="new_password"
91+
id="new_password"
92+
value={newPassword}
93+
required
94+
onInput={(e) => setNewPassword(e.currentTarget.value)}
95+
className="shadow-sm focus:ring-black focus:border-black block w-full sm:text-sm border-gray-300 rounded-sm"
96+
placeholder="Your super secure new password"
97+
/>
98+
</div>
99+
</div>
100+
</div>
101+
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
102+
<div className="py-8 flex justify-end">
103+
<button
104+
type="submit"
105+
className="ml-2 bg-neutral-900 border border-transparent rounded-sm shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black">
106+
Save
107+
</button>
108+
</div>
109+
<hr className="mt-4" />
110+
</div>
111+
</form>
112+
<Modal
113+
heading="Password updated successfully"
114+
description="Your password has been successfully changed."
115+
open={successModalOpen}
116+
handleClose={closeSuccessModal}
117+
/>
118+
</>
119+
);
120+
};
121+
122+
export default ChangePasswordSection;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { SyntheticEvent, useState } from "react";
2+
import Button from "@components/ui/Button";
3+
import { Dialog, DialogContent } from "@components/Dialog";
4+
import { ErrorCode } from "@lib/auth";
5+
import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
6+
import TwoFactorModalHeader from "./TwoFactorModalHeader";
7+
8+
interface DisableTwoFactorAuthModalProps {
9+
/**
10+
* Called when the user closes the modal without disabling two-factor auth
11+
*/
12+
onCancel: () => void;
13+
14+
/**
15+
* Called when the user disables two-factor auth
16+
*/
17+
onDisable: () => void;
18+
}
19+
20+
const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuthModalProps) => {
21+
const [password, setPassword] = useState("");
22+
const [isDisabling, setIsDisabling] = useState(false);
23+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
24+
25+
async function handleDisable(e: SyntheticEvent) {
26+
e.preventDefault();
27+
28+
if (isDisabling) {
29+
return;
30+
}
31+
setIsDisabling(true);
32+
setErrorMessage(null);
33+
34+
try {
35+
const response = await TwoFactorAuthAPI.disable(password);
36+
if (response.status === 200) {
37+
onDisable();
38+
return;
39+
}
40+
41+
const body = await response.json();
42+
if (body.error === ErrorCode.IncorrectPassword) {
43+
setErrorMessage("Password is incorrect.");
44+
} else {
45+
setErrorMessage("Something went wrong.");
46+
}
47+
} catch (e) {
48+
setErrorMessage("Something went wrong.");
49+
console.error("Error disabling two-factor authentication", e);
50+
} finally {
51+
setIsDisabling(false);
52+
}
53+
}
54+
55+
return (
56+
<Dialog open={true}>
57+
<DialogContent>
58+
<TwoFactorModalHeader
59+
title="Disable two-factor authentication"
60+
description="If you need to disable 2FA, we recommend re-enabling it as soon as possible."
61+
/>
62+
63+
<form onSubmit={handleDisable}>
64+
<div className="mb-4">
65+
<label htmlFor="password" className="mt-4 block text-sm font-medium text-gray-700">
66+
Password
67+
</label>
68+
<div className="mt-1">
69+
<input
70+
type="password"
71+
name="password"
72+
id="password"
73+
required
74+
value={password}
75+
onInput={(e) => setPassword(e.currentTarget.value)}
76+
className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
77+
/>
78+
</div>
79+
80+
{errorMessage && <p className="mt-1 text-sm text-red-700">{errorMessage}</p>}
81+
</div>
82+
</form>
83+
84+
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
85+
<Button
86+
type="submit"
87+
className="ml-2"
88+
onClick={handleDisable}
89+
disabled={password.length === 0 || isDisabling}>
90+
Disable
91+
</Button>
92+
<Button color="secondary" onClick={onCancel}>
93+
Cancel
94+
</Button>
95+
</div>
96+
</DialogContent>
97+
</Dialog>
98+
);
99+
};
100+
101+
export default DisableTwoFactorAuthModal;

0 commit comments

Comments
 (0)