From a35ba6ff4bec2f1624e2f0c78430e9b871d01252 Mon Sep 17 00:00:00 2001 From: Posted <271081487+FirTheDeveloper@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:08:33 -0400 Subject: [PATCH 1/3] add zxcvbn and add components --- components/passwordStrengthBar.tsx | 56 ++++++++++++++++++++++++++++++ utils/passwordStrength.ts | 48 +++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 components/passwordStrengthBar.tsx create mode 100644 utils/passwordStrength.ts diff --git a/components/passwordStrengthBar.tsx b/components/passwordStrengthBar.tsx new file mode 100644 index 00000000..32e06909 --- /dev/null +++ b/components/passwordStrengthBar.tsx @@ -0,0 +1,56 @@ +"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", + }; + + return ( +
+
+ {[1, 2, 3, 4].map((bar) => ( +
+ ))} +
+ + {password && ( +
+ + {label} + +
+ )} +
+ ); +} \ No newline at end of file diff --git a/utils/passwordStrength.ts b/utils/passwordStrength.ts new file mode 100644 index 00000000..d55f343c --- /dev/null +++ b/utils/passwordStrength.ts @@ -0,0 +1,48 @@ +import zxcvbn from "zxcvbn"; + +export type PasswordStrength = { + score: 0 | 1 | 2 | 3 | 4; + label: "Weak" | "Okay" | "Strong" | "Super Secure"; + entropy: number; + feedback: string[]; +}; + +export function calculatePasswordStrength( + password: string +): PasswordStrength { + if (!password) { + return { + score: 0, + label: "Weak", + entropy: 0, + feedback: [], + }; + } + + 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", + }; + + const feedback = [ + result.feedback.warning, + ...result.feedback.suggestions, + ].filter(Boolean); + + return { + score, + label: labels[score], + entropy: result.guesses_log10, + feedback, + }; +} \ No newline at end of file From 6201a6f8f512d1d5b8804340017930c6e7cb9d01 Mon Sep 17 00:00:00 2001 From: Posted <271081487+FirTheDeveloper@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:15:30 -0400 Subject: [PATCH 2/3] oh good it works --- components/passwordStrengthBar.tsx | 28 +++++++++++++++----------- pages/login.tsx | 32 ++++++++++++++++++++++-------- utils/passwordStrength.ts | 14 +------------ 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/components/passwordStrengthBar.tsx b/components/passwordStrengthBar.tsx index 32e06909..b78bb721 100644 --- a/components/passwordStrengthBar.tsx +++ b/components/passwordStrengthBar.tsx @@ -27,30 +27,34 @@ export default function PasswordStrengthBar({ 4: "text-green-400", }; + if (!password) return null; + return ( -
+
{[1, 2, 3, 4].map((bar) => (
))}
- {password && ( -
- - {label} +
+ + {label} + + + {score < 3 && ( + + Use a stronger password -
- )} + )} +
); } \ No newline at end of file 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/utils/passwordStrength.ts b/utils/passwordStrength.ts index d55f343c..eac09af1 100644 --- a/utils/passwordStrength.ts +++ b/utils/passwordStrength.ts @@ -3,24 +3,19 @@ import zxcvbn from "zxcvbn"; export type PasswordStrength = { score: 0 | 1 | 2 | 3 | 4; label: "Weak" | "Okay" | "Strong" | "Super Secure"; - entropy: number; - feedback: string[]; }; export function calculatePasswordStrength( - password: string + password: string, ): PasswordStrength { if (!password) { return { score: 0, label: "Weak", - entropy: 0, - feedback: [], }; } const result = zxcvbn(password); - const score = result.score as 0 | 1 | 2 | 3 | 4; const labels: Record< @@ -34,15 +29,8 @@ export function calculatePasswordStrength( 4: "Super Secure", }; - const feedback = [ - result.feedback.warning, - ...result.feedback.suggestions, - ].filter(Boolean); - return { score, label: labels[score], - entropy: result.guesses_log10, - feedback, }; } \ No newline at end of file From 893c1441ccabea955377bf1c1fcce1b32d3cfa80 Mon Sep 17 00:00:00 2001 From: Posted <271081487+FirTheDeveloper@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:20:24 -0400 Subject: [PATCH 3/3] password strength everywhere --- pages/forgot-password.tsx | 104 +++++++++++++++++++++++++------------- pages/welcome.tsx | 67 ++++++++++++++++-------- 2 files changed, 116 insertions(+), 55 deletions(-) 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/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 +