mirror of
https://github.com/wassname/Open-Assistant.git
synced 2026-06-26 16:00:18 +08:00
add captcha to signin page
This commit is contained in:
Vendored
+2
-1
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"python.formatting.provider": "black",
|
||||
"python.analysis.extraPaths": ["${workspaceFolder}/oasst-shared"]
|
||||
"python.analysis.extraPaths": ["${workspaceFolder}/oasst-shared"],
|
||||
"prettier.singleQuote": false
|
||||
}
|
||||
|
||||
@@ -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
@@ -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";
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Vendored
+4
-1
@@ -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 {};
|
||||
|
||||
Reference in New Issue
Block a user