diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx
index bb84ff67..72fe24e3 100644
--- a/src/app/(auth)/register/page.tsx
+++ b/src/app/(auth)/register/page.tsx
@@ -7,9 +7,9 @@ import { useRouter } from "next/navigation";
import LinkText from "@/components/link-text";
import PasswordCriteria from "@/features/auth/components/password-criteria";
+import PasswordValidation from "@/features/auth/components/password-validation";
import TextInputField from "@/features/auth/components/text-input-field";
import ActionButton from "@/features/button/components/action";
-import { useDebounce } from "@/lib/hooks/use-debounce";
import formatApiError from "@/lib/utils/api/format-api-error";
export default function Page() {
@@ -17,48 +17,16 @@ export default function Page() {
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [passwordCriteria, setPasswordCriteria] = useState({});
+ const [showPasswordCriteria, setShowPasswordCriteria] = useState(false);
const router = useRouter();
function passwordIsStrong() {
- return Object.keys(passwordCriteria).length === 0;
+ return Object.values(passwordCriteria).every((value) => value === true);
}
- useDebounce(() => {
- if (password.length === 0) {
- setPasswordCriteria({});
- return;
- }
-
- // Check that the password is strong enough with the API
- fetch("/api/auth/check-password/", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ password }),
- })
- .then((res) => {
- if (res.ok) {
- res.json().then((data) => {
- if (data.is_strong) {
- setPasswordCriteria({});
- return;
- } else {
- setPasswordCriteria(data.criteria || {});
- }
- });
- } else {
- console.error("Fetch error:", res.status);
- }
- })
- .catch((err) => {
- console.error("Fetch error:", err);
- });
- }, [password]);
-
useEffect(() => {
- if (password.length === 0) {
- setPasswordCriteria({});
- return;
- }
+ const { criteria } = PasswordValidation(password);
+ setPasswordCriteria(criteria);
}, [password]);
const stopRefresh = (e: React.FormEvent) => {
@@ -127,10 +95,16 @@ export default function Page() {
placeholder="Password"
value={password}
onChange={setPassword}
+ onFocus={() => setShowPasswordCriteria(true)}
+ onBlur={() => {
+ if (!password || passwordIsStrong()) {
+ setShowPasswordCriteria(false);
+ }
+ }}
/>
{/* Password Errors */}
- {!passwordIsStrong() && (
+ {showPasswordCriteria && (
diff --git a/src/app/(auth)/reset-password/page.tsx b/src/app/(auth)/reset-password/page.tsx
index a9918cc3..04998e5c 100644
--- a/src/app/(auth)/reset-password/page.tsx
+++ b/src/app/(auth)/reset-password/page.tsx
@@ -5,60 +5,28 @@ import React, { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import PasswordCriteria from "@/features/auth/components/password-criteria";
+import PasswordValidation from "@/features/auth/components/password-validation";
import TextInputField from "@/features/auth/components/text-input-field";
import ActionButton from "@/features/button/components/action";
-import { useDebounce } from "@/lib/hooks/use-debounce";
import formatApiError from "@/lib/utils/api/format-api-error";
export default function Page() {
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [passwordCriteria, setPasswordCriteria] = useState({});
+ const [showPasswordCriteria, setShowPasswordCriteria] = useState(false);
const router = useRouter();
const searchParams = useSearchParams();
const pwdResetToken = searchParams.get("token");
function passwordIsStrong() {
- return Object.keys(passwordCriteria).length === 0;
+ return Object.values(passwordCriteria).every((value) => value === true);
}
- useDebounce(() => {
- if (newPassword.length === 0) {
- setPasswordCriteria({});
- return;
- }
-
- // Check that the password is strong enough with the API
- fetch("/api/auth/check-password/", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ password: newPassword }),
- })
- .then((res) => {
- if (res.ok) {
- res.json().then((data) => {
- if (data.is_strong) {
- setPasswordCriteria({});
- return;
- } else {
- setPasswordCriteria(data.criteria || {});
- }
- });
- } else {
- console.error("Fetch error:", res.status);
- }
- })
- .catch((err) => {
- console.error("Fetch error:", err);
- });
- }, [newPassword]);
-
useEffect(() => {
- if (newPassword.length === 0) {
- setPasswordCriteria({});
- return;
- }
+ const { criteria } = PasswordValidation(newPassword);
+ setPasswordCriteria(criteria);
}, [newPassword]);
const stopRefresh = (e: React.FormEvent) => {
@@ -121,9 +89,16 @@ export default function Page() {
placeholder="New Password"
value={newPassword}
onChange={setNewPassword}
+ onFocus={() => setShowPasswordCriteria(true)}
+ onBlur={() => {
+ if (!newPassword || passwordIsStrong()) {
+ setShowPasswordCriteria(false);
+ }
+ }}
/>
- {!passwordIsStrong() && (
+ {/* Password Errors */}
+ {showPasswordCriteria && (
diff --git a/src/features/auth/components/password-criteria.tsx b/src/features/auth/components/password-criteria.tsx
index 0f5ab3a3..ba5635d5 100644
--- a/src/features/auth/components/password-criteria.tsx
+++ b/src/features/auth/components/password-criteria.tsx
@@ -7,21 +7,32 @@ type PasswordCriteriaProps = {
};
export default function PasswordCriteria(props: PasswordCriteriaProps) {
+ const allCriteriaMet = Object.values(props.criteria).every((value) => value);
+
return (
-
Your password must:
- {Object.entries(props.criteria).map(([key, value], index) => (
-
- {value ?
:
}
- {key}
+ {allCriteriaMet ? (
+
+
+ Password is strong!
- ))}
+ ) : (
+ <>
+
Your password must:
+ {Object.entries(props.criteria).map(([key, value], index) => (
+
+ {value ? : }
+ {key}
+
+ ))}
+ >
+ )}
);
}
diff --git a/src/features/auth/components/password-validation.ts b/src/features/auth/components/password-validation.ts
new file mode 100644
index 00000000..6031b0fd
--- /dev/null
+++ b/src/features/auth/components/password-validation.ts
@@ -0,0 +1,33 @@
+const MIN_LENGTH = 8;
+const SPECIAL_CHARACTERS = `!"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~`;
+
+export const PASSWORD_CRITERIA = {
+ LENGTH: `be at least ${MIN_LENGTH} characters long`,
+ LOWER: "contain at least one lowercase letter",
+ UPPER: "contain at least one uppercase letter",
+ DIGIT: "contain at least one digit",
+ SPECIAL: "contain at least one special character",
+} as const;
+
+export type PasswordCriteriaResult = {
+ [key: string]: boolean;
+};
+
+export default function PasswordValidation(password: string): {
+ isStrong: boolean;
+ criteria: PasswordCriteriaResult;
+} {
+ const criteria: PasswordCriteriaResult = {
+ [PASSWORD_CRITERIA.LENGTH]: password.length >= MIN_LENGTH,
+ [PASSWORD_CRITERIA.LOWER]: /[a-z]/.test(password),
+ [PASSWORD_CRITERIA.UPPER]: /[A-Z]/.test(password),
+ [PASSWORD_CRITERIA.DIGIT]: /\d/.test(password),
+ [PASSWORD_CRITERIA.SPECIAL]: new RegExp(
+ `[${SPECIAL_CHARACTERS.replace(/[\\^\-\]]/g, "\\$&")}]`,
+ ).test(password),
+ };
+
+ const isStrong = Object.values(criteria).every((passed) => passed);
+
+ return { isStrong, criteria };
+}
diff --git a/src/features/auth/components/text-input-field.tsx b/src/features/auth/components/text-input-field.tsx
index f888d5e5..caca388d 100644
--- a/src/features/auth/components/text-input-field.tsx
+++ b/src/features/auth/components/text-input-field.tsx
@@ -9,10 +9,12 @@ type TextInputFieldProps = {
placeholder: string;
value: string;
onChange: (value: string) => void;
+ onFocus?: () => void;
+ onBlur?: () => void;
};
export default function TextInputField(props: TextInputFieldProps) {
- const { type, placeholder, value, onChange } = props;
+ const { type, placeholder, value, onChange, onFocus, onBlur } = props;
const [showPassword, setShowPassword] = useState(false);
return (
@@ -22,6 +24,8 @@ export default function TextInputField(props: TextInputFieldProps) {
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
+ onFocus={onFocus}
+ onBlur={onBlur}
className={
"w-full rounded-full border px-4 py-2 focus:outline-none focus:ring-2" +
(type === "password" ? " pr-10" : "")