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",
|
"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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user