mirror of
https://github.com/Sosokker/B2D-Ventures.git
synced 2025-12-18 13:34:06 +01:00
feat: add captcha for authentication and check for password complexity
This commit is contained in:
parent
e90335a029
commit
0e35ebf33d
19
package-lock.json
generated
19
package-lock.json
generated
@ -8,6 +8,7 @@
|
||||
"name": "b2d-ventures",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@hcaptcha/react-hcaptcha": "^1.11.0",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@mdxeditor/editor": "^3.15.0",
|
||||
"@nextui-org/calendar": "^2.0.12",
|
||||
@ -1033,6 +1034,24 @@
|
||||
"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": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz",
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"pretty": "prettier --write \"./**/*.{js,jsx,mjs,cjs,ts,tsx,json}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcaptcha/react-hcaptcha": "^1.11.0",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@mdxeditor/editor": "^3.15.0",
|
||||
"@nextui-org/calendar": "^2.0.12",
|
||||
|
||||
@ -10,6 +10,9 @@ export async function login(formData: FormData) {
|
||||
const data = {
|
||||
email: formData.get("email") as string,
|
||||
password: formData.get("password") as string,
|
||||
options: {
|
||||
captchaToken: formData.get("captchaToken") as string,
|
||||
},
|
||||
};
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword(data);
|
||||
@ -28,6 +31,9 @@ export async function signup(formData: FormData) {
|
||||
const data = {
|
||||
email: formData.get("email") as string,
|
||||
password: formData.get("password") as string,
|
||||
options: {
|
||||
captchaToken: formData.get("captchaToken") as string,
|
||||
},
|
||||
};
|
||||
|
||||
const { error } = await supabase.auth.signUp(data);
|
||||
|
||||
@ -1,21 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { login } from "./action";
|
||||
import { LoginFormSchema } from "@/types/schemas/authentication.schema";
|
||||
import toast from "react-hot-toast";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
export function LoginForm() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
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>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = { email, password };
|
||||
const formData = { email, password, options: { captchaToken } };
|
||||
|
||||
const result = LoginFormSchema.safeParse(formData);
|
||||
|
||||
@ -31,13 +34,18 @@ export function LoginForm() {
|
||||
setErrors({});
|
||||
|
||||
const form = new FormData();
|
||||
form.append("email", email);
|
||||
form.append("password", password);
|
||||
form.set("email", email);
|
||||
form.set("password", password);
|
||||
if (captchaToken) {
|
||||
form.set("captchaToken", captchaToken);
|
||||
}
|
||||
|
||||
try {
|
||||
await login(form);
|
||||
captcha.current?.resetCaptcha();
|
||||
toast.success("Login succesfully!");
|
||||
} catch (authError: any) {
|
||||
captcha.current?.resetCaptcha();
|
||||
setErrors((prevErrors) => ({
|
||||
...prevErrors,
|
||||
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>}
|
||||
</div>
|
||||
<HCaptcha
|
||||
ref={captcha}
|
||||
sitekey={process.env.NEXT_PUBLIC_SITEKEY!}
|
||||
onVerify={(token) => {
|
||||
setCaptchaToken(token);
|
||||
}}
|
||||
/>
|
||||
{errors.server && <p className="text-red-600">{errors.server}</p>}
|
||||
<Button id="login" type="submit">
|
||||
Login
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "next/navigation";
|
||||
import toast from "react-hot-toast";
|
||||
import { signup } from "./action";
|
||||
import { signupSchema } from "@/types/schemas/authentication.schema";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
|
||||
export function SignupForm() {
|
||||
const router = useRouter();
|
||||
@ -14,6 +15,8 @@ export function SignupForm() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = 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>) => {
|
||||
event.preventDefault();
|
||||
@ -30,15 +33,21 @@ export function SignupForm() {
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("email", email);
|
||||
formData.append("password", password);
|
||||
formData.append("confirmPassword", confirmPassword);
|
||||
formData.set("email", email);
|
||||
formData.set("password", password);
|
||||
formData.set("confirmPassword", confirmPassword);
|
||||
|
||||
if (captchaToken) {
|
||||
formData.set("captchaToken", captchaToken);
|
||||
}
|
||||
|
||||
try {
|
||||
await signup(formData);
|
||||
captcha.current?.resetCaptcha();
|
||||
toast.success("Account created successfully!");
|
||||
router.push("/");
|
||||
} catch (error: any) {
|
||||
captcha.current?.resetCaptcha();
|
||||
setError(error.message);
|
||||
}
|
||||
};
|
||||
@ -69,6 +78,13 @@ export function SignupForm() {
|
||||
placeholder="Confirm Password"
|
||||
required
|
||||
/>
|
||||
<HCaptcha
|
||||
ref={captcha}
|
||||
sitekey={process.env.NEXT_PUBLIC_SITEKEY!}
|
||||
onVerify={(token) => {
|
||||
setCaptchaToken(token);
|
||||
}}
|
||||
/>
|
||||
{error && <p className="text-red-600">{error}</p>}
|
||||
<Button id="signup" type="submit">
|
||||
Sign Up
|
||||
|
||||
@ -3,13 +3,48 @@ import * as z from "zod";
|
||||
export const LoginFormSchema = z.object({
|
||||
email: z.string().email("Invalid email address"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters"),
|
||||
options: z
|
||||
.object({
|
||||
captchaToken: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const signupSchema = z
|
||||
.object({
|
||||
email: z.string().email("Invalid email format"),
|
||||
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, {
|
||||
message: "Passwords must match",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user