diff --git a/bun.lock b/bun.lock index c3edf056c..be36d61e0 100644 --- a/bun.lock +++ b/bun.lock @@ -52,6 +52,7 @@ "@vercel/otel": "^1.13.0", "@vercel/speed-insights": "^1.2.0", "ansis": "^3.17.0", + "botid": "^1.5.8", "cheerio": "^1.0.0", "chrono-node": "^2.8.4", "class-variance-authority": "^0.7.1", @@ -1316,6 +1317,8 @@ "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "botid": ["botid@1.5.8", "", { "peerDependencies": { "next": "*", "react": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["next", "react"] }, "sha512-1A/TvyoLtYLlncd30Uyp6ErAEHj4lSpOKqYTJSxX+aSaFUyYSX6rZuYDSX7Zp1kRnoraWHa/4K72mHcjRsUIlQ=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], diff --git a/next.config.mjs b/next.config.mjs index d813236b4..6904e5f70 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,3 +1,5 @@ +import { withBotId } from 'botid/next/config'; + /** @type {import('next').NextConfig} */ const config = { eslint: { @@ -82,4 +84,6 @@ const config = { skipTrailingSlashRedirect: true, } -export default config +const exportedConfig = process.env.NEXT_PUBLIC_USE_BOT_ID === '1' ? withBotId(config) : config + +export default exportedConfig diff --git a/package.json b/package.json index 042f5a8b8..0385e7cc9 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "@vercel/otel": "^1.13.0", "@vercel/speed-insights": "^1.2.0", "ansis": "^3.17.0", + "botid": "^1.5.8", "cheerio": "^1.0.0", "chrono-node": "^2.8.4", "class-variance-authority": "^0.7.1", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7ec7b1ee7..163a2d2d3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -10,11 +10,19 @@ import { GTMHead } from '@/features/google-tag-manager' import { Toaster } from '@/ui/primitives/toaster' import { Analytics } from '@vercel/analytics/next' import { SpeedInsights } from '@vercel/speed-insights/next' +import { BotIdClient } from 'botid/client' import Head from 'next/head' import { Metadata } from 'next/types' import { Suspense } from 'react' import { Body } from './layout.client' +const protectedRoutes = [ + { + path: '/sign-up', + method: 'POST', + }, +] + export const metadata: Metadata = { metadataBase: new URL(BASE_URL), title: { @@ -42,6 +50,7 @@ export default function RootLayout({ + diff --git a/src/configs/flags.ts b/src/configs/flags.ts index 7171d183e..cac8bf13c 100644 --- a/src/configs/flags.ts +++ b/src/configs/flags.ts @@ -1,4 +1,5 @@ export const ALLOW_SEO_INDEXING = process.env.ALLOW_SEO_INDEXING === '1' +export const USE_BOT_ID = process.env.NEXT_PUBLIC_USE_BOT_ID === '1' export const VERBOSE = process.env.NEXT_PUBLIC_VERBOSE === '1' export const INCLUDE_BILLING = process.env.NEXT_PUBLIC_INCLUDE_BILLING === '1' export const USE_MOCK_DATA = diff --git a/src/server/auth/auth-actions.ts b/src/server/auth/auth-actions.ts index 56687b8ea..367625529 100644 --- a/src/server/auth/auth-actions.ts +++ b/src/server/auth/auth-actions.ts @@ -1,5 +1,6 @@ 'use server' +import { USE_BOT_ID } from '@/configs/flags' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { USER_MESSAGES } from '@/configs/user-messages' import { actionClient } from '@/lib/clients/action' @@ -12,17 +13,19 @@ import { shouldWarnAboutAlternateEmail, validateEmail, } from '@/server/auth/validate-email' -import { Provider } from '@supabase/supabase-js' +import { checkBotId } from 'botid/server' import { returnValidationErrors } from 'next-safe-action' import { headers } from 'next/headers' import { redirect } from 'next/navigation' import { z } from 'zod' import { forgotPasswordSchema, signInSchema, signUpSchema } from './auth.types' +const ProviderSchema = z.enum(['google', 'github']) + export const signInWithOAuthAction = actionClient .schema( z.object({ - provider: z.string() as unknown as z.ZodType, + provider: ProviderSchema, returnTo: relativeUrlSchema.optional(), }) ) @@ -89,6 +92,40 @@ export const signUpAction = actionClient }) } + // bot detection + if (USE_BOT_ID) { + const verification = await checkBotId() + + if (verification.isBot) { + l.warn( + { + key: 'sign_up_action:bot_detection_triggered', + context: { + email, + verification, + }, + }, + `Bot detection prevented sign up for: ${email}` + ) + + return returnServerError( + 'Access denied. Please contact support if this issue persists.' + ) + } else { + l.info( + { + key: 'sign_up_action:bot_detection_passed', + context: { + email, + verification, + }, + }, + `Bot detection passed sign up for: ${email}` + ) + } + } + + // email validation const validationResult = await validateEmail(email) if (validationResult?.data) { @@ -103,6 +140,7 @@ export const signUpAction = actionClient } } + // sign up const { error } = await supabase.auth.signUp({ email, password,