mirror of
https://github.com/MAGICGrants/campaign-site.git
synced 2026-01-09 12:27:59 -05:00
Merge pull request #106 from MAGICGrants/cloudflare-captcha
Cloudflare captcha
This commit is contained in:
@@ -11,6 +11,9 @@ NEXTAUTH_URL_INTERNAL="http://localhost:3000"
|
||||
NEXTAUTH_SECRET=""
|
||||
USER_SETTINGS_JWT_SECRET=""
|
||||
|
||||
NEXT_PUBLIC_TURNSTILE_SITEKEY=""
|
||||
TURNSTILE_SECRET=""
|
||||
|
||||
SMTP_HOST="sandbox.smtp.mailtrap.io"
|
||||
SMTP_PORT="2525"
|
||||
SMTP_USER=""
|
||||
|
||||
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
DATABASE_URL=${{ secrets.DATABASE_URL }} \
|
||||
NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }} \
|
||||
USER_SETTINGS_JWT_SECRET=${{ secrets.USER_SETTINGS_JWT_SECRET }} \
|
||||
TURNSTILE_SECRET=${{ secrets.TURNSTILE_SECRET }} \
|
||||
STRAPI_API_URL=${{ secrets.STRAPI_API_URL }} \
|
||||
STRAPI_API_TOKEN=${{ secrets.STRAPI_API_TOKEN }} \
|
||||
SMTP_USER=${{ secrets.SMTP_USER }} \
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { signIn, useSession } from 'next-auth/react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Turnstile } from '@marsidev/react-turnstile'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
|
||||
@@ -12,10 +13,12 @@ import { Button } from './ui/button'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { useFundSlug } from '../utils/use-fund-slug'
|
||||
import Spinner from './Spinner'
|
||||
import { env } from '../env.mjs'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
turnstileToken: z.string().min(1),
|
||||
})
|
||||
|
||||
type LoginFormInputs = z.infer<typeof schema>
|
||||
@@ -50,6 +53,7 @@ function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Pr
|
||||
redirect: false,
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
turnstileToken: data.turnstileToken,
|
||||
})
|
||||
|
||||
if (result?.error) {
|
||||
@@ -122,6 +126,13 @@ function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Pr
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Turnstile
|
||||
siteKey={env.NEXT_PUBLIC_TURNSTILE_SITEKEY}
|
||||
onError={() => form.setValue('turnstileToken', '', { shouldValidate: true })}
|
||||
onExpire={() => form.setValue('turnstileToken', '', { shouldValidate: true })}
|
||||
onSuccess={(token) => form.setValue('turnstileToken', token, { shouldValidate: true })}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row space-x-2">
|
||||
<Button
|
||||
className="grow basis-0"
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Turnstile } from '@marsidev/react-turnstile'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
|
||||
import { DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
|
||||
import { Input } from './ui/input'
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form'
|
||||
import { Button } from './ui/button'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import Spinner from './Spinner'
|
||||
import { env } from '../env.mjs'
|
||||
|
||||
const schema = z.object({
|
||||
turnstileToken: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
})
|
||||
|
||||
@@ -60,6 +63,13 @@ function PasswordResetFormModal({ close }: Props) {
|
||||
)}
|
||||
/>
|
||||
|
||||
<Turnstile
|
||||
siteKey={env.NEXT_PUBLIC_TURNSTILE_SITEKEY}
|
||||
onError={() => form.setValue('turnstileToken', '', { shouldValidate: true })}
|
||||
onExpire={() => form.setValue('turnstileToken', '', { shouldValidate: true })}
|
||||
onSuccess={(token) => form.setValue('turnstileToken', token, { shouldValidate: true })}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={!form.formState.isValid || form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting && <Spinner />} Reset Password
|
||||
</Button>
|
||||
|
||||
@@ -39,9 +39,12 @@ import {
|
||||
} from './ui/command'
|
||||
import { cn } from '../utils/cn'
|
||||
import { Checkbox } from './ui/checkbox'
|
||||
import { Turnstile } from '@marsidev/react-turnstile'
|
||||
import { env } from '../env.mjs'
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
turnstileToken: z.string().min(1),
|
||||
firstName: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -525,6 +528,13 @@ function RegisterFormModal({ close, openLoginModal }: Props) {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Turnstile
|
||||
siteKey={env.NEXT_PUBLIC_TURNSTILE_SITEKEY}
|
||||
onError={() => form.setValue('turnstileToken', '', { shouldValidate: true })}
|
||||
onExpire={() => form.setValue('turnstileToken', '', { shouldValidate: true })}
|
||||
onSuccess={(token) => form.setValue('turnstileToken', token, { shouldValidate: true })}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -40,6 +40,8 @@ services:
|
||||
NEXTAUTH_URL_INTERNAL: http://localhost:3000
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
|
||||
USER_SETTINGS_JWT_SECRET: ${USER_SETTINGS_JWT_SECRET}
|
||||
TURNSTILE_SECRET: ${TURNSTILE_SECRET}
|
||||
NEXT_PUBLIC_TURNSTILE_SITEKEY: ${NEXT_PUBLIC_TURNSTILE_SITEKEY}
|
||||
|
||||
STRAPI_API_URL: ${STRAPI_API_URL}
|
||||
STRAPI_API_TOKEN: ${STRAPI_API_TOKEN}
|
||||
|
||||
7
env.mjs
7
env.mjs
@@ -13,6 +13,8 @@ export const env = createEnv({
|
||||
NEXTAUTH_SECRET: z.string().min(32),
|
||||
USER_SETTINGS_JWT_SECRET: z.string().min(32),
|
||||
|
||||
TURNSTILE_SECRET: z.string().min(1),
|
||||
|
||||
STRAPI_API_URL: z.string().url(),
|
||||
STRAPI_API_TOKEN: z.string().length(256),
|
||||
STRAPI_CDN_HOST: z.string().min(1).optional(),
|
||||
@@ -64,6 +66,7 @@ export const env = createEnv({
|
||||
NEXT_PUBLIC_FIRO_APPLICATION_RECIPIENT: z.string().email(),
|
||||
NEXT_PUBLIC_PRIVACY_GUIDES_APPLICATION_RECIPIENT: z.string().email(),
|
||||
NEXT_PUBLIC_GENERAL_APPLICATION_RECIPIENT: z.string().email(),
|
||||
NEXT_PUBLIC_TURNSTILE_SITEKEY: z.string().min(1),
|
||||
},
|
||||
/*
|
||||
* Due to how Next.js bundles environment variables on Edge and Client,
|
||||
@@ -77,6 +80,8 @@ export const env = createEnv({
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
USER_SETTINGS_JWT_SECRET: process.env.USER_SETTINGS_JWT_SECRET,
|
||||
|
||||
TURNSTILE_SECRET: process.env.TURNSTILE_SECRET,
|
||||
|
||||
NEXT_PUBLIC_STRAPI_URL: process.env.NEXT_PUBLIC_STRAPI_URL,
|
||||
STRAPI_API_URL: process.env.STRAPI_API_URL,
|
||||
STRAPI_API_TOKEN: process.env.STRAPI_API_TOKEN,
|
||||
@@ -117,6 +122,8 @@ export const env = createEnv({
|
||||
|
||||
PRINTFUL_API_KEY: process.env.PRINTFUL_API_KEY,
|
||||
|
||||
NEXT_PUBLIC_TURNSTILE_SITEKEY: process.env.NEXT_PUBLIC_TURNSTILE_SITEKEY,
|
||||
|
||||
NEXT_PUBLIC_MONERO_APPLICATION_RECIPIENT: process.env.NEXT_PUBLIC_MONERO_APPLICATION_RECIPIENT,
|
||||
NEXT_PUBLIC_FIRO_APPLICATION_RECIPIENT: process.env.NEXT_PUBLIC_FIRO_APPLICATION_RECIPIENT,
|
||||
NEXT_PUBLIC_PRIVACY_GUIDES_APPLICATION_RECIPIENT:
|
||||
|
||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -14,6 +14,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@hookform/resolvers": "^3.6.0",
|
||||
"@keycloak/keycloak-admin-client": "^24.0.5",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@prisma/client": "^5.15.1",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
@@ -2777,6 +2778,16 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@marsidev/react-turnstile": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.1.0.tgz",
|
||||
"integrity": "sha512-X7bP9ZYutDd+E+klPYF+/BJHqEyyVkN4KKmZcNRr84zs3DcMoftlMAuoKqNSnqg0HE7NQ1844+TLFSJoztCdSA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.2 || ^18.0.0 || ^19.0",
|
||||
"react-dom": "^17.0.2 || ^18.0.0 || ^19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@hookform/resolvers": "^3.6.0",
|
||||
"@keycloak/keycloak-admin-client": "^24.0.5",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@prisma/client": "^5.15.1",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
|
||||
@@ -6,6 +6,7 @@ import axios from 'axios'
|
||||
import { env } from '../../../env.mjs'
|
||||
import { KeycloakJwtPayload } from '../../../server/types'
|
||||
import { refreshToken } from '../../../server/utils/auth'
|
||||
import { isTurnstileValid } from '../../../server/utils/turnstile'
|
||||
|
||||
export const authOptions: AuthOptions = {
|
||||
callbacks: {
|
||||
@@ -47,8 +48,13 @@ export const authOptions: AuthOptions = {
|
||||
credentials: {
|
||||
email: { label: 'Email', type: 'email' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
turnstileToken: { label: 'Turnstile token', type: 'password' },
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
if (await isTurnstileValid(credentials?.turnstileToken || '')) {
|
||||
throw new Error('INVALID_TURNSTILE_TOKEN')
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: keycloakToken } = await axios.post(
|
||||
`${env.KEYCLOAK_URL}/realms/${env.KEYCLOAK_REALM_NAME}/protocol/openid-connect/token`,
|
||||
|
||||
@@ -8,12 +8,14 @@ import { keycloak, transporter } from '../services'
|
||||
import { env } from '../../env.mjs'
|
||||
import { fundSlugs } from '../../utils/funds'
|
||||
import { UserSettingsJwtPayload } from '../types'
|
||||
import { isTurnstileValid } from '../utils/turnstile'
|
||||
|
||||
export const authRouter = router({
|
||||
register: publicProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
turnstileToken: z.string().min(1),
|
||||
firstName: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -91,6 +93,10 @@ export const authRouter = router({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
if (await isTurnstileValid(input.turnstileToken)) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'INVALID_TURNSTILE_TOKEN' })
|
||||
}
|
||||
|
||||
await authenticateKeycloakClient()
|
||||
|
||||
let user: { id: string }
|
||||
@@ -202,8 +208,12 @@ export const authRouter = router({
|
||||
}),
|
||||
|
||||
requestPasswordReset: publicProcedure
|
||||
.input(z.object({ email: z.string().email() }))
|
||||
.input(z.object({ turnstileToken: z.string().min(1), email: z.string().email() }))
|
||||
.mutation(async ({ input }) => {
|
||||
if (await isTurnstileValid(input.turnstileToken)) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'INVALID_TURNSTILE_TOKEN' })
|
||||
}
|
||||
|
||||
await authenticateKeycloakClient()
|
||||
const users = await keycloak.users.find({ email: input.email })
|
||||
const user = users[0]
|
||||
|
||||
11
server/utils/turnstile.ts
Normal file
11
server/utils/turnstile.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import axios from 'axios'
|
||||
import { env } from '../../env.mjs'
|
||||
|
||||
export async function isTurnstileValid(token: string) {
|
||||
const { data: turnstileResult } = await axios.post(
|
||||
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
||||
{ response: token, secret: env.TURNSTILE_SECRET }
|
||||
)
|
||||
|
||||
return turnstileResult.success
|
||||
}
|
||||
Reference in New Issue
Block a user