From a6fcf0dc1e44a8e9fb03b57c32d9438a9875e776 Mon Sep 17 00:00:00 2001 From: notmd Date: Wed, 25 Jan 2023 17:44:59 +0700 Subject: [PATCH 01/22] wip --- website/.env | 2 ++ website/package-lock.json | 16 ++++++++++++++++ website/package.json | 1 + website/src/components/CloudfareCaptcha.tsx | 8 ++++++++ website/types/env.d.ts | 8 ++++++++ 5 files changed, 35 insertions(+) create mode 100644 website/src/components/CloudfareCaptcha.tsx create mode 100644 website/types/env.d.ts diff --git a/website/.env b/website/.env index 65d8b88e..4bbfe001 100644 --- a/website/.env +++ b/website/.env @@ -14,3 +14,5 @@ NEXTAUTH_SECRET=O/M2uIbGj+lDD2oyNa8ax4jEOJqCPJzO53UbWShmq98= EMAIL_SERVER_HOST=localhost EMAIL_SERVER_PORT=1025 EMAIL_FROM=info@example.com + +NEXT_PUBLIC_CLOUDFARE_CAPTCHA_SITE_KEY=1x0000000000000000000000000000000AA diff --git a/website/package-lock.json b/website/package-lock.json index 348d9fad..12e69cba 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -17,6 +17,7 @@ "@emotion/styled": "^11.10.5", "@headlessui/react": "^1.7.7", "@heroicons/react": "^2.0.13", + "@marsidev/react-turnstile": "^0.0.7", "@next-auth/prisma-adapter": "^1.0.5", "@next/font": "^13.1.0", "@prisma/client": "^4.7.1", @@ -5541,6 +5542,15 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@marsidev/react-turnstile": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-0.0.7.tgz", + "integrity": "sha512-BWXZ6/ddE96cP/U3jkLO8wbJi6qOpE4wH67h6EkD57TRy4RSauBBYKRZkuUEpfgm2wdWoPukPz4LfnoV5KJGrQ==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@mdx-js/mdx": { "version": "1.6.22", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz", @@ -41629,6 +41639,12 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "@marsidev/react-turnstile": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-0.0.7.tgz", + "integrity": "sha512-BWXZ6/ddE96cP/U3jkLO8wbJi6qOpE4wH67h6EkD57TRy4RSauBBYKRZkuUEpfgm2wdWoPukPz4LfnoV5KJGrQ==", + "requires": {} + }, "@mdx-js/mdx": { "version": "1.6.22", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz", diff --git a/website/package.json b/website/package.json index 40d279fe..a0112756 100644 --- a/website/package.json +++ b/website/package.json @@ -34,6 +34,7 @@ "@emotion/styled": "^11.10.5", "@headlessui/react": "^1.7.7", "@heroicons/react": "^2.0.13", + "@marsidev/react-turnstile": "^0.0.7", "@next-auth/prisma-adapter": "^1.0.5", "@next/font": "^13.1.0", "@prisma/client": "^4.7.1", diff --git a/website/src/components/CloudfareCaptcha.tsx b/website/src/components/CloudfareCaptcha.tsx new file mode 100644 index 00000000..d4d827f4 --- /dev/null +++ b/website/src/components/CloudfareCaptcha.tsx @@ -0,0 +1,8 @@ +import { Turnstile } from "@marsidev/react-turnstile"; +import { forwardRef } from "react"; + +export const CloudFareCatpcha = forwardRef((ref, props) => { + return ; +}); + +CloudFareCatpcha.displayName = "CloudFareCatpcha"; diff --git a/website/types/env.d.ts b/website/types/env.d.ts new file mode 100644 index 00000000..959662db --- /dev/null +++ b/website/types/env.d.ts @@ -0,0 +1,8 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + NODE_ENV: "development" | "production"; + NEXT_PUBLIC_CLOUDFARE_CAPTCHA_SITE_KEY: string; + } + } +} From 3c9e5388baa176f40341f61ed01716c45e3ff1c8 Mon Sep 17 00:00:00 2001 From: notmd Date: Thu, 26 Jan 2023 01:00:31 +0700 Subject: [PATCH 02/22] add captcha to signin page --- .vscode/settings.json | 3 +- docker-compose.yaml | 2 + website/.env | 3 +- website/src/components/CloudfareCaptcha.tsx | 8 --- website/src/components/CloudflareCaptcha.tsx | 20 +++++++ website/src/lib/captcha.ts | 56 +++++++++++++++++++ website/src/middleware.ts | 25 ++++++++- website/src/pages/api/auth/invalid-captcha.ts | 7 +++ website/src/pages/auth/signin.tsx | 19 +++++-- website/types/env.d.ts | 5 +- 10 files changed, 132 insertions(+), 16 deletions(-) delete mode 100644 website/src/components/CloudfareCaptcha.tsx create mode 100644 website/src/components/CloudflareCaptcha.tsx create mode 100644 website/src/lib/captcha.ts create mode 100644 website/src/pages/api/auth/invalid-captcha.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 56a51f78..c59304cb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "python.formatting.provider": "black", - "python.analysis.extraPaths": ["${workspaceFolder}/oasst-shared"] + "python.analysis.extraPaths": ["${workspaceFolder}/oasst-shared"], + "prettier.singleQuote": false } diff --git a/docker-compose.yaml b/docker-compose.yaml index 908457cd..4376e25d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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 diff --git a/website/.env b/website/.env index 4bbfe001..a5bb7e0a 100644 --- a/website/.env +++ b/website/.env @@ -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 diff --git a/website/src/components/CloudfareCaptcha.tsx b/website/src/components/CloudfareCaptcha.tsx deleted file mode 100644 index d4d827f4..00000000 --- a/website/src/components/CloudfareCaptcha.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Turnstile } from "@marsidev/react-turnstile"; -import { forwardRef } from "react"; - -export const CloudFareCatpcha = forwardRef((ref, props) => { - return ; -}); - -CloudFareCatpcha.displayName = "CloudFareCatpcha"; diff --git a/website/src/components/CloudflareCaptcha.tsx b/website/src/components/CloudflareCaptcha.tsx new file mode 100644 index 00000000..ad0104f5 --- /dev/null +++ b/website/src/components/CloudflareCaptcha.tsx @@ -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>((props, ref) => { + const { colorMode } = useColorMode(); + return ( + + ); +}); + +CloudFlareCatpcha.displayName = "CloudFlareCatpcha"; diff --git a/website/src/lib/captcha.ts b/website/src/lib/captcha.ts new file mode 100644 index 00000000..2eb4f0a6 --- /dev/null +++ b/website/src/lib/captcha.ts @@ -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 => { + 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; +}; diff --git a/website/src/middleware.ts b/website/src/middleware.ts index 21eeaaa7..87651301 100644 --- a/website/src/middleware.ts +++ b/website/src/middleware.ts @@ -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; diff --git a/website/src/pages/api/auth/invalid-captcha.ts b/website/src/pages/api/auth/invalid-captcha.ts new file mode 100644 index 00000000..86fe7eab --- /dev/null +++ b/website/src/pages/api/auth/invalid-captcha.ts @@ -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", + }); +} diff --git a/website/src/pages/auth/signin.tsx b/website/src/pages/auth/signin.tsx index d171182d..3036a0b9 100644 --- a/website/src/pages/auth/signin.tsx +++ b/website/src/pages/auth/signin.tsx @@ -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 = { @@ -39,6 +42,7 @@ const errorMessages: Record = { 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(); return (
@@ -90,7 +100,8 @@ function Signin({ providers }: SigninProps) { placeholder="Email Address" {...register("email")} /> - }> + + } mt="4"> Continue with Email diff --git a/website/types/env.d.ts b/website/types/env.d.ts index 959662db..79fc51fc 100644 --- a/website/types/env.d.ts +++ b/website/types/env.d.ts @@ -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 {}; From 1af8079e17df57b1eb30052001769e669c5728d1 Mon Sep 17 00:00:00 2001 From: notmd Date: Thu, 26 Jan 2023 01:02:27 +0700 Subject: [PATCH 03/22] remove debug code --- website/src/pages/auth/signin.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/website/src/pages/auth/signin.tsx b/website/src/pages/auth/signin.tsx index 3036a0b9..8f1edf9f 100644 --- a/website/src/pages/auth/signin.tsx +++ b/website/src/pages/auth/signin.tsx @@ -66,13 +66,12 @@ function Signin({ providers }: SigninProps) { } }, [router]); - const signinWithEmail = async (data: { email: string }) => { - const res = await signIn(email.id, { + const signinWithEmail = (data: { email: string }) => { + signIn(email.id, { callbackUrl: "/dashboard", email: data.email, captcha: captcha.current?.getResponse(), }); - console.log(res); }; const { colorMode } = useColorMode(); From 0d629c19cd5375562857df75013c08c0e15b301c Mon Sep 17 00:00:00 2001 From: notmd Date: Thu, 26 Jan 2023 02:13:30 +0700 Subject: [PATCH 04/22] fix test --- website/cypress/e2e/auth/signin.cy.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/website/cypress/e2e/auth/signin.cy.ts b/website/cypress/e2e/auth/signin.cy.ts index 2a651f1f..04a042cf 100644 --- a/website/cypress/e2e/auth/signin.cy.ts +++ b/website/cypress/e2e/auth/signin.cy.ts @@ -14,11 +14,20 @@ describe("signin flow", () => { cy.request("GET", "/api/auth/csrf") .then((response) => { const csrfToken = response.body.csrfToken; - cy.request("POST", "/api/auth/signin/email", { - callbackUrl: "/", - email: emailAddress, - csrfToken, - json: "true", + cy.request({ + form: true, + method: "POST", + url: "/api/auth/signin/email", + body: { + callbackUrl: "/", + email: emailAddress, + csrfToken, + json: "true", + captcha: "XXXX.DUMMY.TOKEN.XXXX", + }, + headers: { + "content-type": "application/x-www-form-urlencoded", + }, }); }) .then((response) => { From 877aabfef1af6a969bc0b8afff7088b33ffa32c9 Mon Sep 17 00:00:00 2001 From: notmd Date: Thu, 26 Jan 2023 02:15:06 +0700 Subject: [PATCH 05/22] check for null --- website/src/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/middleware.ts b/website/src/middleware.ts index 87651301..0bfb4527 100644 --- a/website/src/middleware.ts +++ b/website/src/middleware.ts @@ -23,7 +23,7 @@ export const config = { 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); + const res = await checkCaptcha(data.get("captcha")?.toString(), req.ip); if (res.success) { return NextResponse.next(); From d456cffc4a4d0bc8dbeebc3f708902301a250c36 Mon Sep 17 00:00:00 2001 From: notmd Date: Thu, 26 Jan 2023 02:49:22 +0700 Subject: [PATCH 06/22] support json content-type --- website/cypress/e2e/auth/signin.cy.ts | 4 ---- website/cypress/support/commands.ts | 15 ++++++++++----- website/src/middleware.ts | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/website/cypress/e2e/auth/signin.cy.ts b/website/cypress/e2e/auth/signin.cy.ts index 04a042cf..9d6e63e1 100644 --- a/website/cypress/e2e/auth/signin.cy.ts +++ b/website/cypress/e2e/auth/signin.cy.ts @@ -15,7 +15,6 @@ describe("signin flow", () => { .then((response) => { const csrfToken = response.body.csrfToken; cy.request({ - form: true, method: "POST", url: "/api/auth/signin/email", body: { @@ -25,9 +24,6 @@ describe("signin flow", () => { json: "true", captcha: "XXXX.DUMMY.TOKEN.XXXX", }, - headers: { - "content-type": "application/x-www-form-urlencoded", - }, }); }) .then((response) => { diff --git a/website/cypress/support/commands.ts b/website/cypress/support/commands.ts index 096720d0..3939c685 100644 --- a/website/cypress/support/commands.ts +++ b/website/cypress/support/commands.ts @@ -58,11 +58,16 @@ Cypress.Commands.add("signInWithEmail", (emailAddress) => { cy.request("GET", "/api/auth/csrf") .then((response) => { const csrfToken = response.body.csrfToken; - cy.request("POST", "/api/auth/signin/email", { - callbackUrl: "/", - email: emailAddress, - csrfToken, - json: "true", + cy.request({ + method: "POST", + url: "/api/auth/signin/email", + body: { + callbackUrl: "/", + email: emailAddress, + csrfToken, + json: "true", + captcha: "XXXX.DUMMY.TOKEN.XXXX", + }, }); }) .then(() => { diff --git a/website/src/middleware.ts b/website/src/middleware.ts index 0bfb4527..4c85d3ac 100644 --- a/website/src/middleware.ts +++ b/website/src/middleware.ts @@ -22,8 +22,8 @@ export const config = { 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); + const data = await getBody(req); + const res = await checkCaptcha(data?.captcha, req.ip); if (res.success) { return NextResponse.next(); @@ -38,4 +38,16 @@ const middleware = async (req: NextRequestWithAuth) => { return withAuth(req); }; +async function getBody(req: Request): Promise | undefined> { + if (!("body" in req) || !req.body || req.method !== "POST") return; + + const contentType = req.headers.get("content-type"); + if (contentType?.includes("application/json")) { + return await req.json(); + } else if (contentType?.includes("application/x-www-form-urlencoded")) { + const params = new URLSearchParams(await req.text()); + return Object.fromEntries(params); + } +} + export default middleware; From a7ff5e5f213ed6a263a33ee307ad9c7e744de952 Mon Sep 17 00:00:00 2001 From: notmd Date: Sun, 29 Jan 2023 20:42:23 +0700 Subject: [PATCH 07/22] add comment --- website/src/lib/captcha.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/website/src/lib/captcha.ts b/website/src/lib/captcha.ts index 2eb4f0a6..384f69dd 100644 --- a/website/src/lib/captcha.ts +++ b/website/src/lib/captcha.ts @@ -40,6 +40,7 @@ export const checkCaptcha = async ( }; }; +// This function hasn't been tested yet, Cloudflare doesn't send `action` and `cdata` with a demo key. const getSuccess = (response: CheckCaptchaResponse, action: string | undefined, cdata: string | undefined) => { if (action === undefined && cdata === undefined) { return response.success; From f75d02c33a725ad0389d5050707d2ca19ccbac8b Mon Sep 17 00:00:00 2001 From: notmd Date: Sun, 29 Jan 2023 22:59:45 +0700 Subject: [PATCH 08/22] use `signIn` callback instead middleware --- website/src/middleware.ts | 37 +------------------ website/src/pages/api/auth/[...nextauth].ts | 31 +++++++++++++++- website/src/pages/api/auth/invalid-captcha.ts | 7 ---- 3 files changed, 30 insertions(+), 45 deletions(-) delete mode 100644 website/src/pages/api/auth/invalid-captcha.ts diff --git a/website/src/middleware.ts b/website/src/middleware.ts index 4c85d3ac..21eeaaa7 100644 --- a/website/src/middleware.ts +++ b/website/src/middleware.ts @@ -1,7 +1,5 @@ -import { NextResponse } from "next/server"; -import { NextRequestWithAuth, withAuth } from "next-auth/middleware"; +export { default } from "next-auth/middleware"; -import { checkCaptcha } from "./lib/captcha"; /** * Guards these pages and redirects them to the sign in page. */ @@ -16,38 +14,5 @@ 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 getBody(req); - const res = await checkCaptcha(data?.captcha, 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); -}; - -async function getBody(req: Request): Promise | undefined> { - if (!("body" in req) || !req.body || req.method !== "POST") return; - - const contentType = req.headers.get("content-type"); - if (contentType?.includes("application/json")) { - return await req.json(); - } else if (contentType?.includes("application/x-www-form-urlencoded")) { - const params = new URLSearchParams(await req.text()); - return Object.fromEntries(params); - } -} - -export default middleware; diff --git a/website/src/pages/api/auth/[...nextauth].ts b/website/src/pages/api/auth/[...nextauth].ts index 3d3dbaa4..065d67f4 100644 --- a/website/src/pages/api/auth/[...nextauth].ts +++ b/website/src/pages/api/auth/[...nextauth].ts @@ -1,11 +1,13 @@ import { PrismaAdapter } from "@next-auth/prisma-adapter"; import { boolean } from "boolean"; +import { NextApiRequest, NextApiResponse } from "next"; import type { AuthOptions } from "next-auth"; import NextAuth from "next-auth"; import { Provider } from "next-auth/providers"; import CredentialsProvider from "next-auth/providers/credentials"; import DiscordProvider from "next-auth/providers/discord"; import EmailProvider from "next-auth/providers/email"; +import { checkCaptcha } from "src/lib/captcha"; import prisma from "src/lib/prismadb"; import { generateUsername } from "unique-username-generator"; @@ -74,7 +76,7 @@ const adminUserMap = process.env.ADMIN_USERS.split(",").reduce((result, entry) = return result; }, new Map()); -export const authOptions: AuthOptions = { +const authOptions: AuthOptions = { // Ensure we can store user data in a database. adapter: PrismaAdapter(prisma), providers, @@ -153,4 +155,29 @@ export const authOptions: AuthOptions = { }, }; -export default NextAuth(authOptions); +export default function auth(req: NextApiRequest, res: NextApiResponse) { + return NextAuth(req, res, { + ...authOptions, + callbacks: { + ...authOptions.callbacks, + async signIn({ account }) { + if (account.provider !== "email") { + return true; + } + + const captcha = req.body.captcha; + // https://stackoverflow.com/questions/66111742/get-the-client-ip-on-nextjs-and-use-ssr + const forwarded = req.headers["x-forwarded-for"]; + const ip = typeof forwarded === "string" ? forwarded.split(/, /)[0] : req.socket.remoteAddress; + + const res = await checkCaptcha(captcha, ip); + + if (res.success) { + return true; + } + + return "/auth/signin?error=InvalidCaptcha"; + }, + }, + }); +} diff --git a/website/src/pages/api/auth/invalid-captcha.ts b/website/src/pages/api/auth/invalid-captcha.ts deleted file mode 100644 index 86fe7eab..00000000 --- a/website/src/pages/api/auth/invalid-captcha.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; - -export default function handler(_: NextApiRequest, res: NextApiResponse) { - return res.status(200).json({ - url: "/auth/signin?error=InvalidCaptcha", - }); -} From fce158b8d32be89a176814ffa10facebe86d229f Mon Sep 17 00:00:00 2001 From: notmd Date: Sun, 29 Jan 2023 23:18:08 +0700 Subject: [PATCH 09/22] disable sigin button until captcha success --- website/src/pages/auth/signin.tsx | 73 +++++++++++++++++-------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/website/src/pages/auth/signin.tsx b/website/src/pages/auth/signin.tsx index 8f1edf9f..3a2f48ff 100644 --- a/website/src/pages/auth/signin.tsx +++ b/website/src/pages/auth/signin.tsx @@ -6,7 +6,7 @@ import { GetServerSideProps } from "next"; import Head from "next/head"; import Link from "next/link"; import { useRouter } from "next/router"; -import { ClientSafeProvider, getProviders, signIn } from "next-auth/react"; +import { getProviders, signIn } from "next-auth/react"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import React, { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; @@ -66,19 +66,10 @@ function Signin({ providers }: SigninProps) { } }, [router]); - const signinWithEmail = (data: { email: string }) => { - signIn(email.id, { - callbackUrl: "/dashboard", - email: data.email, - captcha: captcha.current?.getResponse(), - }); - }; - 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(); + return (
@@ -87,25 +78,8 @@ function Signin({ providers }: SigninProps) { - {credentials && } - {email && ( -
- - - - } mt="4"> - Continue with Email - - -
- )} + {credentials && } + {email && } {discord && (