diff --git a/components/passwordStrengthBar.tsx b/components/passwordStrengthBar.tsx new file mode 100644 index 00000000..b78bb721 --- /dev/null +++ b/components/passwordStrengthBar.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { calculatePasswordStrength } from "@/utils/passwordStrength"; + +interface PasswordStrengthBarProps { + password: string; +} + +export default function PasswordStrengthBar({ + password, +}: PasswordStrengthBarProps) { + const { score, label } = calculatePasswordStrength(password); + + const colors = { + 0: "bg-zinc-700", + 1: "bg-red-500", + 2: "bg-yellow-400", + 3: "bg-lime-400", + 4: "bg-green-500", + }; + + const textColors = { + 0: "text-zinc-500", + 1: "text-red-400", + 2: "text-yellow-400", + 3: "text-lime-400", + 4: "text-green-400", + }; + + if (!password) return null; + + return ( +
+
+ {[1, 2, 3, 4].map((bar) => ( +
+ ))} +
+ +
+ + {label} + + + {score < 3 && ( + + Use a stronger password + + )} +
+
+ ); +} \ No newline at end of file diff --git a/pages/forgot-password.tsx b/pages/forgot-password.tsx index 1ab2fcf1..2ff84cdc 100644 --- a/pages/forgot-password.tsx +++ b/pages/forgot-password.tsx @@ -8,6 +8,8 @@ import Input from "@/components/input"; import Button from "@/components/button"; import { Dialog } from "@headlessui/react"; import { IconX } from "@tabler/icons-react"; +import PasswordStrengthBar from "@/components/passwordStrengthBar"; +import { calculatePasswordStrength } from "@/utils/passwordStrength"; type FormData = { username: string; @@ -35,7 +37,7 @@ function getAvatarBgColor(displayName: string): string { const ForgotPassword: NextPage = () => { const [selectedSlide, setSelectedSlide] = useState(0); const [code, setCode] = useState(""); - const [userId, setUserId] = useState(null); + const [userId, setUserId] = useState(null); const [resetDisplayName, setResetDisplayName] = useState(""); const [resetThumbnail, setResetThumbnail] = useState(""); const [error, setError] = useState(null); @@ -53,6 +55,9 @@ const ForgotPassword: NextPage = () => { const usernameForm = useForm(); const passwordForm = useForm(); + + const resetPassword = passwordForm.watch("password") || ""; + const resetPasswordStrength = calculatePasswordStrength(resetPassword); const startReset = async () => { setError(null); @@ -263,53 +268,82 @@ const ForgotPassword: NextPage = () => { {selectedSlide === 3 && ( <>

- Set your new password + Set your new password

+

- Enter and confirm your new password. + Enter and confirm your new password.

+ {error && ( -

{error}

+

+ {error} +

)} + -
- - - value === passwordForm.getValues("password") || "Passwords must match", - })} - label="Confirm password" - /> -
- + +
+ { + const { score } = calculatePasswordStrength(value); + + return ( + score >= 3 || + "Your password must be at least Strong" + ); + }, + })} + label="New password" + /> + + +
+ + + value === passwordForm.getValues("password") || + "Passwords must match", + })} + label="Confirm password" + /> + +
+ + -
-

- Don’t share your password. Don’t use the same password as your Roblox account. -

- + +
+ +

+ Don’t share your password. Don’t use the same password as your + Roblox account. +

+
- )} + )}
diff --git a/pages/login.tsx b/pages/login.tsx index d42a646c..a7cd0255 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -22,6 +22,8 @@ import { sessionPrimaryButtonClass, sessionSecondaryButtonClass, } from "@/components/sessions/shell"; +import PasswordStrengthBar from "@/components/passwordStrengthBar"; +import { calculatePasswordStrength } from "@/utils/passwordStrength"; const oauthButtonClass = "w-full flex items-center justify-center gap-2 rounded-xl bg-zinc-100 px-4 py-2.5 text-sm font-medium text-zinc-700 transition hover:bg-zinc-200 disabled:opacity-50 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"; @@ -166,7 +168,6 @@ const Login: NextPage = () => { const usernameCheckTimeout = useRef(null); const effectiveOAuthOnly = oauthOnly && isOAuthAvailable; - useEffect(() => { loginMethods.reset(); signupMethods.reset(); @@ -182,6 +183,9 @@ const Login: NextPage = () => { if (usernameCheckTimeout.current) clearTimeout(usernameCheckTimeout.current); }, [mode]); + + const signupPassword = signupMethods.watch("password") || ""; + const passwordStrength = calculatePasswordStrength(signupPassword); useEffect(() => { let isMounted = true; @@ -772,9 +776,11 @@ const Login: NextPage = () => {

Set a password

+

Choose a secure password for your account.

+
{ required: "Password is required", minLength: { value: 7, - message: - "Password must be at least 7 characters", + message: "Password must be at least 7 characters", }, - pattern: { - value: /^(?=.*[0-9!@#$%^&*])/, - message: - "Password must contain at least one number or special character", + validate: (value) => { + const { score } = calculatePasswordStrength(value); + + if (score < 3) { + return "Password is not strong enough"; + } + + return true; }, })} /> + + + { "Passwords must match", })} /> +
+ Continue
+ {(isRobloxOAuth || isDiscordOAuth || isGoogleOAuth) && ( @@ -842,6 +857,7 @@ const Login: NextPage = () => { )} +

Don't share your password. Don't use the same password as your Roblox account. diff --git a/pages/welcome.tsx b/pages/welcome.tsx index aa5ffc26..7c10091b 100644 --- a/pages/welcome.tsx +++ b/pages/welcome.tsx @@ -10,6 +10,8 @@ import axios from "axios"; import { toast } from "react-hot-toast"; import { IconCheck, IconEye, IconEyeOff, IconInfoCircle, IconX } from "@tabler/icons-react"; import { getContrastColor } from "@/utils/color"; +import PasswordStrengthBar from "@/components/passwordStrengthBar"; +import { calculatePasswordStrength } from "@/utils/passwordStrength"; type FormData = { username: string; @@ -33,6 +35,8 @@ const Login: NextPage = () => { const [ocLoading, setOcLoading] = useState(false); const [showApiKey, setShowApiKey] = useState(false); const testedKey = useRef(null); + const signupPassword = signupform.watch("password") || ""; + const signupPasswordStrength = calculatePasswordStrength(signupPassword); async function createAccount() { setIsLoading(true); @@ -350,35 +354,52 @@ const Login: NextPage = () => { {!isRegistered && (

-

Make your Orbit account

+

+ Make your Orbit account +

+

You need to create an Orbit account to continue

+ - + + {signupform.formState.errors.username && (

{signupform.formState.errors.username.message}

)} - +
+ { + const { score } = calculatePasswordStrength(value); + + return ( + score >= 3 || + "Password must be at least Strong" + ); + }, + })} + label="Password" + /> + + +
+ {signupform.formState.errors.password && (

{signupform.formState.errors.password.message} @@ -389,12 +410,13 @@ const Login: NextPage = () => { type="password" {...signupform.register("verifypassword", { required: "Please verify your password", - validate: value => - value === signupform.getValues('password') || - "Passwords do not match" + validate: (value) => + value === signupform.getValues("password") || + "Passwords do not match", })} label="Verify password" /> + {signupform.formState.errors.verifypassword && (

{signupform.formState.errors.verifypassword.message} @@ -411,14 +433,19 @@ const Login: NextPage = () => { > Back +

diff --git a/utils/passwordStrength.ts b/utils/passwordStrength.ts new file mode 100644 index 00000000..eac09af1 --- /dev/null +++ b/utils/passwordStrength.ts @@ -0,0 +1,36 @@ +import zxcvbn from "zxcvbn"; + +export type PasswordStrength = { + score: 0 | 1 | 2 | 3 | 4; + label: "Weak" | "Okay" | "Strong" | "Super Secure"; +}; + +export function calculatePasswordStrength( + password: string, +): PasswordStrength { + if (!password) { + return { + score: 0, + label: "Weak", + }; + } + + const result = zxcvbn(password); + const score = result.score as 0 | 1 | 2 | 3 | 4; + + const labels: Record< + 0 | 1 | 2 | 3 | 4, + PasswordStrength["label"] + > = { + 0: "Weak", + 1: "Weak", + 2: "Okay", + 3: "Strong", + 4: "Super Secure", + }; + + return { + score, + label: labels[score], + }; +} \ No newline at end of file