add captcha to signin page

This commit is contained in:
notmd
2023-01-26 01:00:31 +07:00
parent a6fcf0dc1e
commit 3c9e5388ba
10 changed files with 132 additions and 16 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
{
"python.formatting.provider": "black",
"python.analysis.extraPaths": ["${workspaceFolder}/oasst-shared"]
"python.analysis.extraPaths": ["${workspaceFolder}/oasst-shared"],
"prettier.singleQuote": false
}
+2
View File
@@ -125,6 +125,8 @@ services:
- EMAIL_FROM=info@example.com
- NEXTAUTH_URL=http://localhost:3000
- DEBUG_LOGIN=true
- NEXT_PUBLIC_CLOUDFARE_CAPTCHA_SITE_KEY=1x00000000000000000000AA
- CLOUDFLARE_CAPTCHA_SERCERT_KEY=1x0000000000000000000000000000000AA
depends_on:
webdb:
condition: service_healthy
+2 -1
View File
@@ -15,4 +15,5 @@ EMAIL_SERVER_HOST=localhost
EMAIL_SERVER_PORT=1025
EMAIL_FROM=info@example.com
NEXT_PUBLIC_CLOUDFARE_CAPTCHA_SITE_KEY=1x0000000000000000000000000000000AA
NEXT_PUBLIC_CLOUDFLARE_CAPTCHA_SITE_KEY=1x00000000000000000000AA
CLOUDFLARE_CAPTCHA_SERCERT_KEY=1x0000000000000000000000000000000AA
@@ -1,8 +0,0 @@
import { Turnstile } from "@marsidev/react-turnstile";
import { forwardRef } from "react";
export const CloudFareCatpcha = forwardRef((ref, props) => {
return <Turnstile siteKey={process.env.NEXT_PUBLIC_CLOUDFARE_CAPTCHA_SITE_KEY} />;
});
CloudFareCatpcha.displayName = "CloudFareCatpcha";
@@ -0,0 +1,20 @@
import { useColorMode } from "@chakra-ui/react";
import { Turnstile, TurnstileInstance, TurnstileProps } from "@marsidev/react-turnstile";
import { forwardRef } from "react";
export const CloudFlareCatpcha = forwardRef<TurnstileInstance, Omit<TurnstileProps, "siteKey">>((props, ref) => {
const { colorMode } = useColorMode();
return (
<Turnstile
ref={ref}
{...props}
siteKey={process.env.NEXT_PUBLIC_CLOUDFLARE_CAPTCHA_SITE_KEY}
options={{
theme: colorMode,
...props.options,
}}
/>
);
});
CloudFlareCatpcha.displayName = "CloudFlareCatpcha";
+56
View File
@@ -0,0 +1,56 @@
type CaptchaErrorCode =
| "missing-input-secret"
| "invalid-input-secret"
| "missing-input-response"
| "invalid-input-response"
| "bad-request"
| "timeout-or-duplicate"
| "internal-error";
type CheckCaptchaResponse = {
success: boolean;
challenge_ts?: string;
hostname: string;
"error-codes": CaptchaErrorCode[];
action?: string;
cdata?: string;
};
// https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
export const checkCaptcha = async (
token: string,
ipAdress: string,
options?: { cdata?: string; action?: string }
): Promise<CheckCaptchaResponse> => {
const data = new FormData();
data.append("secret", process.env.CLOUDFLARE_CAPTCHA_SERCERT_KEY);
data.append("response", token);
data.append("remoteip", ipAdress);
const result = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
body: data,
method: "POST",
});
const res: CheckCaptchaResponse = await result.json();
return {
...res,
success: getSuccess(res, options?.action, options?.cdata),
};
};
const getSuccess = (response: CheckCaptchaResponse, action: string | undefined, cdata: string | undefined) => {
if (action === undefined && cdata === undefined) {
return response.success;
}
if (action) {
if (cdata) {
return response.action === action && response.cdata === cdata;
}
return response.action === action;
}
return false;
};
+24 -1
View File
@@ -1,5 +1,7 @@
export { default } from "next-auth/middleware";
import { NextResponse } from "next/server";
import { NextRequestWithAuth, withAuth } from "next-auth/middleware";
import { checkCaptcha } from "./lib/captcha";
/**
* Guards these pages and redirects them to the sign in page.
*/
@@ -14,5 +16,26 @@ export const config = {
"/tasks/:path*",
"/leaderboard",
"/messages/:path*",
"/api/auth/signin/email",
],
};
const middleware = async (req: NextRequestWithAuth) => {
if (req.method === "POST" && req.nextUrl.pathname === "/api/auth/signin/email") {
const data = await req.formData();
const res = await checkCaptcha(data.get("captcha").toString(), req.ip);
if (res.success) {
return NextResponse.next();
}
const url = req.nextUrl.clone();
url.pathname = "/api/auth/invalid-captcha";
return NextResponse.redirect(url);
}
return withAuth(req);
};
export default middleware;
@@ -0,0 +1,7 @@
import { NextApiRequest, NextApiResponse } from "next";
export default function handler(_: NextApiRequest, res: NextApiResponse) {
return res.status(200).json({
url: "/auth/signin?error=InvalidCaptcha",
});
}
+15 -4
View File
@@ -1,5 +1,6 @@
import { Button, ButtonProps, Input, Stack, useColorModeValue } from "@chakra-ui/react";
import { useColorMode } from "@chakra-ui/react";
import { TurnstileInstance } from "@marsidev/react-turnstile";
import { Bug, Github, Mail } from "lucide-react";
import { GetServerSideProps } from "next";
import Head from "next/head";
@@ -7,9 +8,10 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { ClientSafeProvider, getProviders, signIn } from "next-auth/react";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { AuthLayout } from "src/components/AuthLayout";
import { CloudFlareCatpcha } from "src/components/CloudflareCaptcha";
import { Footer } from "src/components/Footer";
import { Header } from "src/components/Header";
import { Discord } from "src/components/Icons/Discord";
@@ -26,6 +28,7 @@ export type SignInErrorTypes =
| "EmailSignin"
| "CredentialsSignin"
| "SessionRequired"
| "InvalidCaptcha"
| "default";
const errorMessages: Record<SignInErrorTypes, string> = {
@@ -39,6 +42,7 @@ const errorMessages: Record<SignInErrorTypes, string> = {
EmailSignin: "The e-mail could not be sent.",
CredentialsSignin: "Sign in failed. Check the details you provided are correct.",
SessionRequired: "Please sign in to access this page.",
InvalidCaptcha: "Invalid captcha",
default: "Unable to sign in.",
};
@@ -62,14 +66,20 @@ function Signin({ providers }: SigninProps) {
}
}, [router]);
const signinWithEmail = (data: { email: string }) => {
signIn(email.id, { callbackUrl: "/dashboard", email: data.email });
const signinWithEmail = async (data: { email: string }) => {
const res = await signIn(email.id, {
callbackUrl: "/dashboard",
email: data.email,
captcha: captcha.current?.getResponse(),
});
console.log(res);
};
const { colorMode } = useColorMode();
const bgColorClass = colorMode === "light" ? "bg-gray-50" : "bg-chakra-gray-900";
const buttonBgColor = colorMode === "light" ? "#2563eb" : "#2563eb";
const { register, handleSubmit } = useForm<{ email: string }>();
const captcha = useRef<TurnstileInstance>();
return (
<div className={bgColorClass}>
<Head>
@@ -90,7 +100,8 @@ function Signin({ providers }: SigninProps) {
placeholder="Email Address"
{...register("email")}
/>
<SigninButton data-cy="signin-email-button" leftIcon={<Mail />}>
<CloudFlareCatpcha options={{ size: "invisible" }} ref={captcha}></CloudFlareCatpcha>
<SigninButton data-cy="signin-email-button" leftIcon={<Mail />} mt="4">
Continue with Email
</SigninButton>
</Stack>
+4 -1
View File
@@ -2,7 +2,10 @@ declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production";
NEXT_PUBLIC_CLOUDFARE_CAPTCHA_SITE_KEY: string;
NEXT_PUBLIC_CLOUDFLARE_CAPTCHA_SITE_KEY: string;
CLOUDFLARE_CAPTCHA_SERCERT_KEY: string;
}
}
}
export {};