Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions components/passwordStrengthBar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mt-2 space-y-1.5">
<div className="flex gap-1.5">
{[1, 2, 3, 4].map((bar) => (
<div
key={bar}
className={`h-1.5 flex-1 rounded-full transition-all duration-300 ${
bar <= score ? colors[score] : "bg-zinc-800"
}`}
/>
))}
</div>

<div className="flex items-center justify-between">
<span
className={`text-xs font-medium transition-colors duration-300 ${textColors[score]}`}
>
{label}
</span>

{score < 3 && (
<span className="text-xs text-zinc-400">
Use a stronger password
</span>
)}
</div>
</div>
);
}
104 changes: 69 additions & 35 deletions pages/forgot-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [resetDisplayName, setResetDisplayName] = useState("");
const [resetThumbnail, setResetThumbnail] = useState("");
const [error, setError] = useState<string | null>(null);
Expand All @@ -53,6 +55,9 @@ const ForgotPassword: NextPage = () => {

const usernameForm = useForm<FormData>();
const passwordForm = useForm<ResetPasswordData>();

const resetPassword = passwordForm.watch("password") || "";
const resetPasswordStrength = calculatePasswordStrength(resetPassword);

const startReset = async () => {
setError(null);
Expand Down Expand Up @@ -263,53 +268,82 @@ const ForgotPassword: NextPage = () => {
{selectedSlide === 3 && (
<>
<h2 className="text-xl font-semibold text-zinc-900 dark:text-white tracking-tight">
Set your new password
Set your new password
</h2>

<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400 mb-6">
Enter and confirm your new password.
Enter and confirm your new password.
</p>

{error && (
<p className="text-center text-red-500 text-sm mb-4">{error}</p>
<p className="text-center text-red-500 text-sm mb-4">
{error}
</p>
)}

<FormProvider {...passwordForm}>
<form className="space-y-4" onSubmit={passwordForm.handleSubmit(finishReset)}>
<Input
type="password"
{...passwordForm.register("password", { required: "You must enter a password" })}
label="New password"
/>
<Input
type="password"
{...passwordForm.register("verifypassword", {
required: "Please confirm your password",
validate: (value) =>
value === passwordForm.getValues("password") || "Passwords must match",
})}
label="Confirm password"
/>
<div className="flex gap-3 pt-1">
<Button
type="button"
classoverride="flex-1 px-5 py-2.5 text-sm font-medium rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700"
onPress={() => setSelectedSlide(2)}
>
Back
</Button>
<form
className="space-y-4"
onSubmit={passwordForm.handleSubmit(finishReset)}
>
<div>
<Input
type="password"
{...passwordForm.register("password", {
required: "You must enter a password",
validate: (value) => {
const { score } = calculatePasswordStrength(value);

return (
score >= 3 ||
"Your password must be at least Strong"
);
},
})}
label="New password"
/>

<PasswordStrengthBar password={resetPassword} />
</div>

<Input
type="password"
{...passwordForm.register("verifypassword", {
required: "Please confirm your password",
validate: (value) =>
value === passwordForm.getValues("password") ||
"Passwords must match",
})}
label="Confirm password"
/>

<div className="flex gap-3 pt-1">
<Button
type="button"
classoverride="flex-1 px-5 py-2.5 text-sm font-medium rounded-xl bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-600 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-700"
onPress={() => setSelectedSlide(2)}
>
Back
</Button>

<Button
type="submit"
classoverride="flex-1 px-6 py-2.5 text-sm font-medium rounded-xl shadow-sm"
disabled={resetPasswordStrength.score < 3}
classoverride="flex-1 px-6 py-2.5 text-sm font-medium rounded-xl shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
workspace
>
Reset password
</Button>
</div>
<p className="mt-4 text-xs text-zinc-500 dark:text-zinc-400 text-center">
Don鈥檛 share your password. Don鈥檛 use the same password as your Roblox account.
</p>
</form>
</Button>
</div>

<p className="mt-4 text-xs text-zinc-500 dark:text-zinc-400 text-center">
Don鈥檛 share your password. Don鈥檛 use the same password as your
Roblox account.
</p>
</form>
</FormProvider>
</>
)}
)}
</div>
</div>
</div>
Expand Down
32 changes: 24 additions & 8 deletions pages/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -166,7 +168,6 @@ const Login: NextPage = () => {
const usernameCheckTimeout = useRef<NodeJS.Timeout | null>(null);

const effectiveOAuthOnly = oauthOnly && isOAuthAvailable;

useEffect(() => {
loginMethods.reset();
signupMethods.reset();
Expand All @@ -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;
Expand Down Expand Up @@ -772,9 +776,11 @@ const Login: NextPage = () => {
<h2 className="text-lg font-semibold tracking-tight text-zinc-900 dark:text-white">
Set a password
</h2>

<p className="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
Choose a secure password for your account.
</p>

<FormProvider {...signupMethods}>
<form
onSubmit={submitSignup(onSubmitSignup)}
Expand All @@ -791,16 +797,22 @@ const Login: NextPage = () => {
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;
},
})}
/>

<PasswordStrengthBar password={signupPassword} />

<Input
label="Verify password"
placeholder="Verify password"
Expand All @@ -814,6 +826,7 @@ const Login: NextPage = () => {
"Passwords must match",
})}
/>

<div className="flex gap-3 pt-4">
<button
type="button"
Expand All @@ -826,14 +839,16 @@ const Login: NextPage = () => {
>
Back
</button>

<AuthSubmitButton
className="flex-1 justify-center"
loading={loading}
disabled={loading}
disabled={loading || passwordStrength.score < 3}
>
Continue
</AuthSubmitButton>
</div>

{(isRobloxOAuth ||
isDiscordOAuth ||
isGoogleOAuth) && (
Expand All @@ -842,6 +857,7 @@ const Login: NextPage = () => {
<OAuthButtons />
</>
)}

<p className="mt-4 text-center text-xs text-zinc-400 dark:text-zinc-500">
Don&apos;t share your password. Don&apos;t use the
same password as your Roblox account.
Expand Down
Loading
Loading