feat: add captcha for authentication and check for password complexity

This commit is contained in:
Sosokker 2024-11-17 16:30:27 +07:00
parent e90335a029
commit 0e35ebf33d
6 changed files with 101 additions and 9 deletions

19
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "b2d-ventures", "name": "b2d-ventures",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@hcaptcha/react-hcaptcha": "^1.11.0",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",
"@mdxeditor/editor": "^3.15.0", "@mdxeditor/editor": "^3.15.0",
"@nextui-org/calendar": "^2.0.12", "@nextui-org/calendar": "^2.0.12",
@ -1033,6 +1034,24 @@
"tslib": "2" "tslib": "2"
} }
}, },
"node_modules/@hcaptcha/loader": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@hcaptcha/loader/-/loader-1.2.4.tgz",
"integrity": "sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw=="
},
"node_modules/@hcaptcha/react-hcaptcha": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.11.0.tgz",
"integrity": "sha512-UKHtzzVMHLTGwab5pgV96UbcXdyh5Qyq8E0G5DTyXq8txMvuDx7rSyC+BneOjWVW0a7O9VuZmkg/EznVLRE45g==",
"dependencies": {
"@babel/runtime": "^7.17.9",
"@hcaptcha/loader": "^1.2.1"
},
"peerDependencies": {
"react": ">= 16.3.0",
"react-dom": ">= 16.3.0"
}
},
"node_modules/@hookform/resolvers": { "node_modules/@hookform/resolvers": {
"version": "3.9.1", "version": "3.9.1",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz",

View File

@ -10,6 +10,7 @@
"pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\"" "pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\""
}, },
"dependencies": { "dependencies": {
"@hcaptcha/react-hcaptcha": "^1.11.0",
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",
"@mdxeditor/editor": "^3.15.0", "@mdxeditor/editor": "^3.15.0",
"@nextui-org/calendar": "^2.0.12", "@nextui-org/calendar": "^2.0.12",

View File

@ -10,6 +10,9 @@ export async function login(formData: FormData) {
const data = { const data = {
email: formData.get("email") as string, email: formData.get("email") as string,
password: formData.get("password") as string, password: formData.get("password") as string,
options: {
captchaToken: formData.get("captchaToken") as string,
},
}; };
const { error } = await supabase.auth.signInWithPassword(data); const { error } = await supabase.auth.signInWithPassword(data);
@ -28,6 +31,9 @@ export async function signup(formData: FormData) {
const data = { const data = {
email: formData.get("email") as string, email: formData.get("email") as string,
password: formData.get("password") as string, password: formData.get("password") as string,
options: {
captchaToken: formData.get("captchaToken") as string,
},
}; };
const { error } = await supabase.auth.signUp(data); const { error } = await supabase.auth.signUp(data);

View File

@ -1,21 +1,24 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useRef, useState } from "react";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { login } from "./action"; import { login } from "./action";
import { LoginFormSchema } from "@/types/schemas/authentication.schema"; import { LoginFormSchema } from "@/types/schemas/authentication.schema";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import HCaptcha from "@hcaptcha/react-hcaptcha";
export function LoginForm() { export function LoginForm() {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [errors, setErrors] = useState<{ email?: string; password?: string; server?: string }>({}); const [errors, setErrors] = useState<{ email?: string; password?: string; server?: string }>({});
const [captchaToken, setCaptchaToken] = useState<string | undefined>(undefined);
const captcha = useRef<HCaptcha | null>(null);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
const formData = { email, password }; const formData = { email, password, options: { captchaToken } };
const result = LoginFormSchema.safeParse(formData); const result = LoginFormSchema.safeParse(formData);
@ -31,13 +34,18 @@ export function LoginForm() {
setErrors({}); setErrors({});
const form = new FormData(); const form = new FormData();
form.append("email", email); form.set("email", email);
form.append("password", password); form.set("password", password);
if (captchaToken) {
form.set("captchaToken", captchaToken);
}
try { try {
await login(form); await login(form);
captcha.current?.resetCaptcha();
toast.success("Login succesfully!"); toast.success("Login succesfully!");
} catch (authError: any) { } catch (authError: any) {
captcha.current?.resetCaptcha();
setErrors((prevErrors) => ({ setErrors((prevErrors) => ({
...prevErrors, ...prevErrors,
server: authError.message || "An error occurred during login.", server: authError.message || "An error occurred during login.",
@ -61,6 +69,13 @@ export function LoginForm() {
/> />
{errors.password && <p className="text-red-600">{errors.password}</p>} {errors.password && <p className="text-red-600">{errors.password}</p>}
</div> </div>
<HCaptcha
ref={captcha}
sitekey={process.env.NEXT_PUBLIC_SITEKEY!}
onVerify={(token) => {
setCaptchaToken(token);
}}
/>
{errors.server && <p className="text-red-600">{errors.server}</p>} {errors.server && <p className="text-red-600">{errors.server}</p>}
<Button id="login" type="submit"> <Button id="login" type="submit">
Login Login

View File

@ -1,12 +1,13 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useRef, useState } from "react";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { signup } from "./action"; import { signup } from "./action";
import { signupSchema } from "@/types/schemas/authentication.schema"; import { signupSchema } from "@/types/schemas/authentication.schema";
import HCaptcha from "@hcaptcha/react-hcaptcha";
export function SignupForm() { export function SignupForm() {
const router = useRouter(); const router = useRouter();
@ -14,6 +15,8 @@ export function SignupForm() {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
const [captchaToken, setCaptchaToken] = useState<string | undefined>(undefined);
const captcha = useRef<HCaptcha | null>(null);
const handleSignup = async (event: React.FormEvent<HTMLFormElement>) => { const handleSignup = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
@ -30,15 +33,21 @@ export function SignupForm() {
} }
const formData = new FormData(); const formData = new FormData();
formData.append("email", email); formData.set("email", email);
formData.append("password", password); formData.set("password", password);
formData.append("confirmPassword", confirmPassword); formData.set("confirmPassword", confirmPassword);
if (captchaToken) {
formData.set("captchaToken", captchaToken);
}
try { try {
await signup(formData); await signup(formData);
captcha.current?.resetCaptcha();
toast.success("Account created successfully!"); toast.success("Account created successfully!");
router.push("/"); router.push("/");
} catch (error: any) { } catch (error: any) {
captcha.current?.resetCaptcha();
setError(error.message); setError(error.message);
} }
}; };
@ -69,6 +78,13 @@ export function SignupForm() {
placeholder="Confirm Password" placeholder="Confirm Password"
required required
/> />
<HCaptcha
ref={captcha}
sitekey={process.env.NEXT_PUBLIC_SITEKEY!}
onVerify={(token) => {
setCaptchaToken(token);
}}
/>
{error && <p className="text-red-600">{error}</p>} {error && <p className="text-red-600">{error}</p>}
<Button id="signup" type="submit"> <Button id="signup" type="submit">
Sign Up Sign Up

View File

@ -3,13 +3,48 @@ import * as z from "zod";
export const LoginFormSchema = z.object({ export const LoginFormSchema = z.object({
email: z.string().email("Invalid email address"), email: z.string().email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters"), password: z.string().min(6, "Password must be at least 6 characters"),
options: z
.object({
captchaToken: z.string().optional(),
})
.optional(),
}); });
export const signupSchema = z export const signupSchema = z
.object({ .object({
email: z.string().email("Invalid email format"), email: z.string().email("Invalid email format"),
password: z.string().min(6, "Password must be at least 6 characters long"), password: z.string().min(6, "Password must be at least 6 characters long"),
confirmPassword: z.string().min(6, "Confirm password must be at least 6 characters long"), confirmPassword: z.string().min(8, "Confirm password must be at least 8 characters long"),
options: z
.object({
captchaToken: z.string().optional(),
})
.optional(),
})
.superRefine(({ password }, checkPassComplexity) => {
const containsUppercase = (ch: string) => /[A-Z]/.test(ch);
const containsLowercase = (ch: string) => /[a-z]/.test(ch);
const containsSpecialChar = (ch: string) =>
// eslint-disable-next-line no-useless-escape
/[`!@#$%^&*()_\-+=\[\]{};':"\\|,.<>\/?~ ]/.test(ch);
let countOfUpperCase = 0,
countOfLowerCase = 0,
countOfNumbers = 0,
countOfSpecialChar = 0;
for (let i = 0; i < password.length; i++) {
let ch = password.charAt(i);
if (!isNaN(+ch)) countOfNumbers++;
else if (containsUppercase(ch)) countOfUpperCase++;
else if (containsLowercase(ch)) countOfLowerCase++;
else if (containsSpecialChar(ch)) countOfSpecialChar++;
}
if (countOfLowerCase < 1 || countOfUpperCase < 1 || countOfSpecialChar < 1 || countOfNumbers < 1) {
checkPassComplexity.addIssue({
code: "custom",
message:
"Password should contain combo of uppercase letters, lowercase letters, numbers, and even some special characters (!, @, $, %, ^, &, *, +, #)",
});
}
}) })
.refine((data) => data.password === data.confirmPassword, { .refine((data) => data.password === data.confirmPassword, {
message: "Passwords must match", message: "Passwords must match",