From 091705150fbd2bfa1f045d27db30251a58b3bf8f Mon Sep 17 00:00:00 2001 From: Din Date: Tue, 12 Dec 2023 15:15:10 +0700 Subject: [PATCH 1/4] popup turnstile --- @api/.dev.example.vars | 3 +- @web/.env.local | 3 +- @web/.env.preview | 1 + @web/.env.production | 1 + @web/app/_providers/query.tsx | 27 +++++++ @web/app/_providers/turnstile.tsx | 57 +++++++++++++ @web/app/layout.tsx | 9 ++- @web/app/page.tsx | 14 ++-- @web/components/profile-dropdown-menu.tsx | 1 + @web/components/turnstile.tsx | 24 ++++++ @web/env.ts | 2 + @web/package.json | 2 + pnpm-lock.yaml | 99 ++++++++++++++--------- 13 files changed, 194 insertions(+), 49 deletions(-) create mode 100644 @web/app/_providers/turnstile.tsx create mode 100644 @web/components/turnstile.tsx diff --git a/@api/.dev.example.vars b/@api/.dev.example.vars index f7ba40c..2763e71 100644 --- a/@api/.dev.example.vars +++ b/@api/.dev.example.vars @@ -5,4 +5,5 @@ GOOGLE_CLIENT_SECRET= RESEND_API_KEY= RESEND_FROM= GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= \ No newline at end of file +GITHUB_CLIENT_SECRET= +TURNSTILE_SECRET_KEY= \ No newline at end of file diff --git a/@web/.env.local b/@web/.env.local index f9feddb..4c79eda 100644 --- a/@web/.env.local +++ b/@web/.env.local @@ -1 +1,2 @@ -NEXT_PUBLIC_API_URL=http://localhost:8000 \ No newline at end of file +NEXT_PUBLIC_API_URL=http://localhost:8000 +NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAAAOd1-P2R_2ooD0h diff --git a/@web/.env.preview b/@web/.env.preview index 63175ae..6f119fd 100644 --- a/@web/.env.preview +++ b/@web/.env.preview @@ -1 +1,2 @@ NEXT_PUBLIC_API_URL=https://preview.api.example.com +NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAAAOd1-P2R_2ooD0h diff --git a/@web/.env.production b/@web/.env.production index 6c01aec..d61aef9 100644 --- a/@web/.env.production +++ b/@web/.env.production @@ -1 +1,2 @@ NEXT_PUBLIC_API_URL=https://api.example.com +NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAAAOd1-P2R_2ooD0h diff --git a/@web/app/_providers/query.tsx b/@web/app/_providers/query.tsx index 4866571..0442b60 100644 --- a/@web/app/_providers/query.tsx +++ b/@web/app/_providers/query.tsx @@ -10,6 +10,7 @@ import { useState } from 'react' import SuperJSON from 'superjson' import { useToast } from '@ui/ui/use-toast' import { store } from './jotai' +import { showTurnstileAtom, turnstileTokenAtom } from './turnstile' export function QueryProvider({ children }: { children: React.ReactNode }) { const { toast } = useToast() @@ -66,6 +67,32 @@ export function QueryProvider({ children }: { children: React.ReactNode }) { return headers }, + async fetch(input, init) { + const method = init?.method?.toUpperCase() ?? 'GET' + if (method === 'POST' && init) { + if (!store.get(turnstileTokenAtom)) { + store.set(showTurnstileAtom, true) + await new Promise((resolve) => { + store.sub(turnstileTokenAtom, () => { + const token = store.get(turnstileTokenAtom) + if (token) { + resolve(token) + } + }) + }) + store.set(showTurnstileAtom, false) + } + + const token = store.get(turnstileTokenAtom) + + init.headers = { + ...init.headers, + 'X-Turnstile-Token': `${token}`, + } + } + + return await fetch(input, init) + }, }), ], }), diff --git a/@web/app/_providers/turnstile.tsx b/@web/app/_providers/turnstile.tsx new file mode 100644 index 0000000..fd85438 --- /dev/null +++ b/@web/app/_providers/turnstile.tsx @@ -0,0 +1,57 @@ +'use client' + +import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile' +import * as Portal from '@radix-ui/react-portal' +import { env } from '@web/env' +import { atom, useAtom } from 'jotai' +import { useTheme } from 'next-themes' +import { useEffect, useId, useRef } from 'react' +import { match } from 'ts-pattern' +import { useIsRendered } from '@ui/hooks/use-is-rendered' +import { cn } from '@ui/lib/utils' + +export const turnstileTokenAtom = atom(null) + +export const showTurnstileAtom = atom(false) + +export default function TurnstileProvider({ children }: { children: React.ReactNode }) { + const { theme } = useTheme() + const [, setTurnstileToken] = useAtom(turnstileTokenAtom) + const [showTurnstile] = useAtom(showTurnstileAtom) + const turnstileRef = useRef(null) + const id = useId() + const isRendered = useIsRendered() + + return ( + <> + {children} + {isRendered && ( + +
+ 'dark' as const) + .with('light', () => 'light' as const) + .otherwise(() => 'auto' as const), + }} + onSuccess={(token) => setTurnstileToken(token)} + onError={() => setTurnstileToken(null)} + onExpire={() => turnstileRef.current?.reset()} + /> +
+
+ )} + + ) +} diff --git a/@web/app/layout.tsx b/@web/app/layout.tsx index 5c6f00d..b7564d8 100644 --- a/@web/app/layout.tsx +++ b/@web/app/layout.tsx @@ -6,6 +6,7 @@ import { Toaster } from '@ui/ui/toaster' import JotaiProvider from './_providers/jotai' import { QueryProvider } from './_providers/query' import { ThemeProvider } from './_providers/theme' +import TurnstileProvider from './_providers/turnstile' const inter = Inter({ subsets: ['latin'] }) @@ -21,9 +22,11 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - -
{children}
-
+ + +
{children}
+
+
diff --git a/@web/app/page.tsx b/@web/app/page.tsx index 14715a6..8161e1c 100644 --- a/@web/app/page.tsx +++ b/@web/app/page.tsx @@ -1,5 +1,6 @@ 'use client' +import { Turnstile } from '@web/components/turnstile' import { api } from '@web/lib/api' import { Button } from '@ui/ui/button' @@ -7,11 +8,14 @@ export default function Home() { const { status, data } = api.ping.useQuery() return (
- - {process.env.NEXT_PUBLIC_API_URL} - - {status} {JSON.stringify(data)} - +
{ + console.log('submit') + }} + > + + +
) } diff --git a/@web/components/profile-dropdown-menu.tsx b/@web/components/profile-dropdown-menu.tsx index bd2ec6f..1e40d07 100644 --- a/@web/components/profile-dropdown-menu.tsx +++ b/@web/components/profile-dropdown-menu.tsx @@ -216,6 +216,7 @@ function WorkspaceListItem(props: { function LogoutDropdownMenuItem() { const [, setAuth] = useAtom(authAtom) + // TODO: call logout api return setAuth(RESET)}>Log out } diff --git a/@web/components/turnstile.tsx b/@web/components/turnstile.tsx new file mode 100644 index 0000000..eed96e1 --- /dev/null +++ b/@web/components/turnstile.tsx @@ -0,0 +1,24 @@ +'use client' + +import { Turnstile as Base, TurnstileInstance } from '@marsidev/react-turnstile' +import { useId, useRef, useState } from 'react' + +export function Turnstile() { + const [token, setToken] = useState('') + const ref = useRef() + const id = useId() + return ( +
+ setToken(token)} + onError={() => console.error('error')} + onExpire={() => ref.current?.reset()} + /> + +
+ ) +} diff --git a/@web/env.ts b/@web/env.ts index 1234046..63ca562 100644 --- a/@web/env.ts +++ b/@web/env.ts @@ -14,6 +14,7 @@ export const env = createEnv({ */ client: { NEXT_PUBLIC_API_URL: z.string().url(), + NEXT_PUBLIC_TURNSTILE_SITE_KEY: z.string(), }, /* * Due to how Next.js bundles environment variables on Edge and Client, @@ -23,5 +24,6 @@ export const env = createEnv({ */ runtimeEnv: { NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, + NEXT_PUBLIC_TURNSTILE_SITE_KEY: process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY, }, }) diff --git a/@web/package.json b/@web/package.json index dadc83b..27a6482 100644 --- a/@web/package.json +++ b/@web/package.json @@ -11,7 +11,9 @@ }, "dependencies": { "@dinstack/ui": "workspace:^", + "@marsidev/react-turnstile": "^0.4.0", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-portal": "^1.0.4", "@t3-oss/env-nextjs": "^0.7.1", "@tanstack/react-query": "^4.36.1", "@trpc/client": "^10.44.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 188ed0a..4f21171 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,7 +131,7 @@ importers: version: 1.0.5(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.0.2 - version: 1.0.2(react@18.2.0) + version: 1.0.2(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-toast': specifier: ^1.1.5 version: 1.1.5(react-dom@18.2.0)(react@18.2.0) @@ -166,9 +166,15 @@ importers: '@dinstack/ui': specifier: workspace:^ version: link:../@ui + '@marsidev/react-turnstile': + specifier: ^0.4.0 + version: 0.4.0(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.2.0) + '@radix-ui/react-portal': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@t3-oss/env-nextjs': specifier: ^0.7.1 version: 0.7.1(typescript@5.3.3)(zod@3.22.4) @@ -1211,6 +1217,16 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@marsidev/react-turnstile@0.4.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-U0lwTWQhP04AnIdWEbLpF/phpqtblNbrnE4yJMdvGyeOZOTmuHW3wB+V8X+BO55pTXZitp2GgJsrGkbUjwJIeQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@neondatabase/serverless@0.6.0: resolution: {integrity: sha512-qXxBRYN0m2v8kVQBfMxbzNGn2xFAhTXFibzQlE++NfJ56Shz3m7+MyBBtXDlEH+3Wfa6lToDXf1MElocY4sJ3w==} dependencies: @@ -1621,7 +1637,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.5 - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -1640,15 +1656,15 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.5 - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-context': 1.0.1(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.43)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-compose-refs@1.0.1(react@18.2.0): + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.43)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: '@types/react': '*' @@ -1658,6 +1674,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.5 + '@types/react': 18.2.43 react: 18.2.0 dev: false @@ -1689,16 +1706,16 @@ packages: dependencies: '@babel/runtime': 7.23.5 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-context': 1.0.1(react@18.2.0) '@radix-ui/react-dismissable-layer': 1.0.5(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-focus-guards': 1.0.1(react@18.2.0) '@radix-ui/react-focus-scope': 1.0.4(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-id': 1.0.1(react@18.2.0) - '@radix-ui/react-portal': 1.0.4(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-presence': 1.0.1(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(react@18.2.0) aria-hidden: 1.2.3 react: 18.2.0 @@ -1734,8 +1751,8 @@ packages: dependencies: '@babel/runtime': 7.23.5 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0) '@radix-ui/react-use-escape-keydown': 1.0.3(react@18.2.0) react: 18.2.0 @@ -1757,11 +1774,11 @@ packages: dependencies: '@babel/runtime': 7.23.5 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-context': 1.0.1(react@18.2.0) '@radix-ui/react-id': 1.0.1(react@18.2.0) '@radix-ui/react-menu': 2.0.6(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -1794,8 +1811,8 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.5 - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -1837,7 +1854,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.5 - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -1858,7 +1875,7 @@ packages: '@babel/runtime': 7.23.5 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-collection': 1.0.3(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-context': 1.0.1(react@18.2.0) '@radix-ui/react-direction': 1.0.1(react@18.2.0) '@radix-ui/react-dismissable-layer': 1.0.5(react-dom@18.2.0)(react@18.2.0) @@ -1866,11 +1883,11 @@ packages: '@radix-ui/react-focus-scope': 1.0.4(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-id': 1.0.1(react@18.2.0) '@radix-ui/react-popper': 1.1.3(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-portal': 1.0.4(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-presence': 1.0.1(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-roving-focus': 1.0.4(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-slot': 1.0.2(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0) aria-hidden: 1.2.3 react: 18.2.0 @@ -1894,9 +1911,9 @@ packages: '@babel/runtime': 7.23.5 '@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-arrow': 1.0.3(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-context': 1.0.1(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(react@18.2.0) '@radix-ui/react-use-rect': 1.0.1(react@18.2.0) @@ -1906,7 +1923,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-portal@1.0.4(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-portal@1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} peerDependencies: '@types/react': '*' @@ -1920,7 +1937,9 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.5 - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -1939,13 +1958,13 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.5 - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-primitive@1.0.3(react-dom@18.2.0)(react@18.2.0): + /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} peerDependencies: '@types/react': '*' @@ -1959,7 +1978,9 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.5 - '@radix-ui/react-slot': 1.0.2(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 + '@types/react-dom': 18.2.17 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -1980,11 +2001,11 @@ packages: '@babel/runtime': 7.23.5 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-collection': 1.0.3(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-context': 1.0.1(react@18.2.0) '@radix-ui/react-direction': 1.0.1(react@18.2.0) '@radix-ui/react-id': 1.0.1(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(react@18.2.0) react: 18.2.0 @@ -2007,18 +2028,18 @@ packages: '@babel/runtime': 7.23.5 '@radix-ui/number': 1.0.1 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-context': 1.0.1(react@18.2.0) '@radix-ui/react-direction': 1.0.1(react@18.2.0) '@radix-ui/react-presence': 1.0.1(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@radix-ui/react-slot@1.0.2(react@18.2.0): + /@radix-ui/react-slot@1.0.2(@types/react@18.2.43)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: '@types/react': '*' @@ -2028,7 +2049,8 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.5 - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) + '@types/react': 18.2.43 react: 18.2.0 dev: false @@ -2048,12 +2070,12 @@ packages: '@babel/runtime': 7.23.5 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-collection': 1.0.3(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.43)(react@18.2.0) '@radix-ui/react-context': 1.0.1(react@18.2.0) '@radix-ui/react-dismissable-layer': 1.0.5(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-portal': 1.0.4(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-presence': 1.0.1(react-dom@18.2.0)(react@18.2.0) - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(react@18.2.0) '@radix-ui/react-use-layout-effect': 1.0.1(react@18.2.0) @@ -2158,7 +2180,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.23.5 - '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.17)(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -2333,7 +2355,6 @@ packages: resolution: {integrity: sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==} dependencies: '@types/react': 18.2.43 - dev: true /@types/react@18.2.43: resolution: {integrity: sha512-nvOV01ZdBdd/KW6FahSbcNplt2jCJfyWdTos61RYHV+FVv5L/g9AOX1bmbVcWcLFL8+KHQfh1zVIQrud6ihyQA==} From 832bd446d2e8e7028060d364c6a6674916096c78 Mon Sep 17 00:00:00 2001 From: Din Date: Tue, 12 Dec 2023 16:12:08 +0700 Subject: [PATCH 2/4] turnstile on api --- @api/env.ts | 1 + @api/trpc.ts | 27 ++++++++++++++++++++++- @web/app/_providers/query.tsx | 4 +++- @web/app/_providers/turnstile.tsx | 13 +++++++++-- @web/components/profile-dropdown-menu.tsx | 2 +- 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/@api/env.ts b/@api/env.ts index 0a5372c..906fc4c 100644 --- a/@api/env.ts +++ b/@api/env.ts @@ -12,6 +12,7 @@ export const envSchema = z.object({ GITHUB_CLIENT_SECRET: z.string(), RESEND_API_KEY: z.string(), RESEND_FROM: z.string(), + TURNSTILE_SECRET_KEY: z.string(), }) export type Env = z.infer diff --git a/@api/trpc.ts b/@api/trpc.ts index 9f4a5ad..ef5598a 100644 --- a/@api/trpc.ts +++ b/@api/trpc.ts @@ -9,7 +9,32 @@ const t = initTRPC.context().create({ export const middleware = t.middleware export const router = t.router -export const procedure = t.procedure +const turnstileMiddleware = middleware(async ({ ctx, next, type }) => { + if (type === 'mutation') { + let formData = new FormData() + formData.append('secret', ctx.env.TURNSTILE_SECRET_KEY) + formData.append('response', ctx.request.headers.get('X-Turnstile-Token')) + formData.append('remoteip', ctx.request.headers.get('CF-Connecting-IP')) + + const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + body: formData, + method: 'POST', + }) + const outcome = (await res.json()) as { success: boolean } + if (!outcome.success) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'You are behaving like an automated bot.', + }) + } + } + + return next({ + ctx, + }) +}) + +export const procedure = t.procedure.use(turnstileMiddleware) const authMiddleware = middleware(async ({ ctx, next }) => { const bearer = ctx.request.headers.get('Authorization') diff --git a/@web/app/_providers/query.tsx b/@web/app/_providers/query.tsx index 0442b60..1d03955 100644 --- a/@web/app/_providers/query.tsx +++ b/@web/app/_providers/query.tsx @@ -10,7 +10,7 @@ import { useState } from 'react' import SuperJSON from 'superjson' import { useToast } from '@ui/ui/use-toast' import { store } from './jotai' -import { showTurnstileAtom, turnstileTokenAtom } from './turnstile' +import { showTurnstileAtom, turnstileRefAtom, turnstileTokenAtom } from './turnstile' export function QueryProvider({ children }: { children: React.ReactNode }) { const { toast } = useToast() @@ -89,6 +89,8 @@ export function QueryProvider({ children }: { children: React.ReactNode }) { ...init.headers, 'X-Turnstile-Token': `${token}`, } + store.set(turnstileTokenAtom, null) + store.get(turnstileRefAtom)?.reset() } return await fetch(input, init) diff --git a/@web/app/_providers/turnstile.tsx b/@web/app/_providers/turnstile.tsx index fd85438..9d488d1 100644 --- a/@web/app/_providers/turnstile.tsx +++ b/@web/app/_providers/turnstile.tsx @@ -14,6 +14,8 @@ export const turnstileTokenAtom = atom(null) export const showTurnstileAtom = atom(false) +export const turnstileRefAtom = atom(null) + export default function TurnstileProvider({ children }: { children: React.ReactNode }) { const { theme } = useTheme() const [, setTurnstileToken] = useAtom(turnstileTokenAtom) @@ -21,6 +23,7 @@ export default function TurnstileProvider({ children }: { children: React.ReactN const turnstileRef = useRef(null) const id = useId() const isRendered = useIsRendered() + const [, _setTurnstileRef] = useAtom(turnstileRefAtom) return ( <> @@ -45,9 +48,15 @@ export default function TurnstileProvider({ children }: { children: React.ReactN .with('light', () => 'light' as const) .otherwise(() => 'auto' as const), }} - onSuccess={(token) => setTurnstileToken(token)} + onSuccess={(token) => { + _setTurnstileRef(turnstileRef.current) + setTurnstileToken(token) + }} onError={() => setTurnstileToken(null)} - onExpire={() => turnstileRef.current?.reset()} + onExpire={() => { + setTurnstileToken(null) + turnstileRef.current?.reset() + }} /> diff --git a/@web/components/profile-dropdown-menu.tsx b/@web/components/profile-dropdown-menu.tsx index 1e40d07..d4f9563 100644 --- a/@web/components/profile-dropdown-menu.tsx +++ b/@web/components/profile-dropdown-menu.tsx @@ -175,7 +175,7 @@ function WorkspaceListItem(props: { organization: props.organization, }) }} - disabled={props.disabled} + disabled={mutation.isLoading || props.disabled} > {mutation.isLoading ? (
From 76248adf2f989feb81f8117485ed810db9b34ae7 Mon Sep 17 00:00:00 2001 From: Din Date: Tue, 12 Dec 2023 16:12:59 +0700 Subject: [PATCH 3/4] fix --- @api/trpc.ts | 2 +- @web/app/_providers/turnstile.tsx | 5 +++-- @web/app/page.tsx | 14 +++++--------- @web/components/turnstile.tsx | 3 ++- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/@api/trpc.ts b/@api/trpc.ts index ef5598a..9685ee3 100644 --- a/@api/trpc.ts +++ b/@api/trpc.ts @@ -11,7 +11,7 @@ export const router = t.router const turnstileMiddleware = middleware(async ({ ctx, next, type }) => { if (type === 'mutation') { - let formData = new FormData() + const formData = new FormData() formData.append('secret', ctx.env.TURNSTILE_SECRET_KEY) formData.append('response', ctx.request.headers.get('X-Turnstile-Token')) formData.append('remoteip', ctx.request.headers.get('CF-Connecting-IP')) diff --git a/@web/app/_providers/turnstile.tsx b/@web/app/_providers/turnstile.tsx index 9d488d1..acaa68b 100644 --- a/@web/app/_providers/turnstile.tsx +++ b/@web/app/_providers/turnstile.tsx @@ -1,11 +1,12 @@ 'use client' -import { Turnstile, TurnstileInstance } from '@marsidev/react-turnstile' +import type { TurnstileInstance } from '@marsidev/react-turnstile' +import { Turnstile } from '@marsidev/react-turnstile' import * as Portal from '@radix-ui/react-portal' import { env } from '@web/env' import { atom, useAtom } from 'jotai' import { useTheme } from 'next-themes' -import { useEffect, useId, useRef } from 'react' +import { useId, useRef } from 'react' import { match } from 'ts-pattern' import { useIsRendered } from '@ui/hooks/use-is-rendered' import { cn } from '@ui/lib/utils' diff --git a/@web/app/page.tsx b/@web/app/page.tsx index 8161e1c..14715a6 100644 --- a/@web/app/page.tsx +++ b/@web/app/page.tsx @@ -1,6 +1,5 @@ 'use client' -import { Turnstile } from '@web/components/turnstile' import { api } from '@web/lib/api' import { Button } from '@ui/ui/button' @@ -8,14 +7,11 @@ export default function Home() { const { status, data } = api.ping.useQuery() return (
-
{ - console.log('submit') - }} - > - - - + + {process.env.NEXT_PUBLIC_API_URL} + + {status} {JSON.stringify(data)} +
) } diff --git a/@web/components/turnstile.tsx b/@web/components/turnstile.tsx index eed96e1..e9efb9c 100644 --- a/@web/components/turnstile.tsx +++ b/@web/components/turnstile.tsx @@ -1,6 +1,7 @@ 'use client' -import { Turnstile as Base, TurnstileInstance } from '@marsidev/react-turnstile' +import type { TurnstileInstance } from '@marsidev/react-turnstile' +import { Turnstile as Base } from '@marsidev/react-turnstile' import { useId, useRef, useState } from 'react' export function Turnstile() { From fb986bbdc0e748e32a7370ff70c0cb29ef3aa2cd Mon Sep 17 00:00:00 2001 From: Din Date: Tue, 12 Dec 2023 16:13:22 +0700 Subject: [PATCH 4/4] trash --- @web/components/turnstile.tsx | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 @web/components/turnstile.tsx diff --git a/@web/components/turnstile.tsx b/@web/components/turnstile.tsx deleted file mode 100644 index e9efb9c..0000000 --- a/@web/components/turnstile.tsx +++ /dev/null @@ -1,25 +0,0 @@ -'use client' - -import type { TurnstileInstance } from '@marsidev/react-turnstile' -import { Turnstile as Base } from '@marsidev/react-turnstile' -import { useId, useRef, useState } from 'react' - -export function Turnstile() { - const [token, setToken] = useState('') - const ref = useRef() - const id = useId() - return ( -
- setToken(token)} - onError={() => console.error('error')} - onExpire={() => ref.current?.reset()} - /> - -
- ) -}