Skip to content

Commit

Permalink
feat: added proof of concept for captcha support
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffdowdle committed Aug 26, 2024
1 parent 22beb71 commit 4c4f517
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 9 deletions.
2 changes: 2 additions & 0 deletions packages/nuxt-ripple/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export default defineNuxtConfig({
}
}
},
recaptchaSecretKey: '',
turnstileSecretKey: '',
public: {
siteUrl: '',
apiUrl: '',
Expand Down
25 changes: 25 additions & 0 deletions packages/nuxt-ripple/server/api/tide/webform_submission/[...].ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,31 @@ import { createProxyMiddleware } from 'http-proxy-middleware'
export const createWebformProxyHandler = async (event: H3Event) => {
const { public: config } = useRuntimeConfig()

// if (!(await verifyCaptcha(event))) {
// console.log('alskdnl')
// sendError(
// event,
// createError({
// statusCode: 400,
// statusMessage: 'CAPTCHA validation error'
// })
// )
// return
// }
try {
await verifyCaptcha(event)
} catch (error) {
console.error(error)
sendError(
event,
createError({
statusCode: 400,
statusMessage: 'CAPTCHA validation error'
})
)
return
}

const proxyMiddleware = createProxyMiddleware({
target: config.tide.baseUrl,
pathRewrite: {
Expand Down
74 changes: 74 additions & 0 deletions packages/nuxt-ripple/server/utils/verifyCaptcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const verifyGoogleRecaptcha = async (runtimeConfig, captchaResponse) => {
try {
const verifyResponse = await $fetch(
'https://www.google.com/recaptcha/api/siteverify',
{
method: 'POST',
body: `secret=${runtimeConfig.recaptchaSecretKey}&response=${captchaResponse}`,
headers: { 'Content-type': 'application/x-www-form-urlencoded' }
}
)

if (!verifyResponse?.success) {
return false
}

return true
} catch (error) {
console.error(error)
return false
}
}

const verifyCloudfareTurnstile = async (runtimeConfig, captchaResponse) => {

Check warning on line 23 in packages/nuxt-ripple/server/utils/verifyCaptcha.ts

View workflow job for this annotation

GitHub Actions / Test

'runtimeConfig' is defined but never used

Check warning on line 23 in packages/nuxt-ripple/server/utils/verifyCaptcha.ts

View workflow job for this annotation

GitHub Actions / Test

'captchaResponse' is defined but never used
// TODO
return false
}

const verify = async (runtimeConfig, captchaType, captchaResponse) => {
console.log(captchaType)
switch (captchaType) {
case 'recaptcha':
return await verifyGoogleRecaptcha(runtimeConfig, captchaResponse)
case 'turnstile':
return await verifyCloudfareTurnstile(runtimeConfig, captchaResponse)
default:
return false
}
}

const verifyCaptcha = async (event) => {
const config = useRuntimeConfig()
const captchaResponse = getHeader(event, 'x-captcha-response')

const webform = await $fetch('/api/tide/webform', {
baseURL: config.apiUrl || '',
params: {
id: 'test_form_jeff'
}
})

if (!webform) {
throw new Error(
`Couldn't get webform data, unable to continue because we don't know if a captcha is required`
)
}

const formHasCaptcha = webform?.captchaConfig?.enabled

if (!formHasCaptcha) {
return true
}

const isValid = await verify(
config,
webform?.captchaConfig?.type,
captchaResponse
)

if (!isValid) {
throw new Error('aslkdnalksdm')
}
}

export default verifyCaptcha
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface Props {
errorMessageTitle?: string
errorMessageHTML: string
schema?: Array<FormKitSchemaNode>
captchaConfig?: any

Check warning on line 14 in packages/ripple-tide-landing-page/components/global/TideLandingPage/WebForm.vue

View workflow job for this annotation

GitHub Actions / Test

Prop 'captchaConfig' requires default value to be set
}
const props = withDefaults(defineProps<Props>(), {
Expand All @@ -23,7 +24,12 @@ const props = withDefaults(defineProps<Props>(), {
const honeypotId = `${props.formId}-important-email`
const { submissionState, submitHandler } = useWebformSubmit(props.formId)
const { submissionState, submitHandler } = useWebformSubmit(
props.formId,
props.captchaConfig
)
const captchaWidgetId = useCaptchaWidget(props.formId, props.captchaConfig)

Check warning on line 32 in packages/ripple-tide-landing-page/components/global/TideLandingPage/WebForm.vue

View workflow job for this annotation

GitHub Actions / Test

'captchaWidgetId' is assigned a value but never used. Allowed unused vars must match /props/u
const serverSuccessRef = ref(null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { TideDynamicPageComponent } from '@dpc-sdp/ripple-tide-api/types'
import { TidePageApi } from '@dpc-sdp/ripple-tide-api'
import type { TideWebform, ApiField } from '@dpc-sdp/ripple-tide-webform/types'
import { getFormSchemaFromMapping } from '@dpc-sdp/ripple-tide-webform/mapping'
import {
getFormSchemaFromMapping,
getCaptchaSettings
} from '@dpc-sdp/ripple-tide-webform/mapping'

const componentMapping = async (field: ApiField, tidePageApi: TidePageApi) => {
return {
Expand All @@ -22,7 +25,8 @@ const componentMapping = async (field: ApiField, tidePageApi: TidePageApi) => {
schema: await getFormSchemaFromMapping(
field.field_paragraph_webform,
tidePageApi
)
),
captchaConfig: getCaptchaSettings(field.field_paragraph_webform)
}
}
export const webformMapping = async (
Expand Down
31 changes: 27 additions & 4 deletions packages/ripple-tide-webform/composables/use-webform-submit.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { $fetch } from 'ofetch'
import { ref, useRuntimeConfig } from '#imports'

export function useWebformSubmit(formId: string) {
const postForm = async (formId: string, formData = {}) => {
export function useWebformSubmit(formId: string, captchaConfig) {
const postForm = async (
formId: string,
formData = {},
maybeCaptchaResponse: string | null
) => {
const { public: config } = useRuntimeConfig()

const formResource = 'webform_submission'
Expand All @@ -29,7 +33,8 @@ export function useWebformSubmit(formId: string) {
site: config.tide.site
},
headers: {
'Content-Type': 'application/vnd.api+json;charset=UTF-8'
'Content-Type': 'application/vnd.api+json;charset=UTF-8',
'x-captcha-response': maybeCaptchaResponse || undefined
}
})

Expand Down Expand Up @@ -87,8 +92,26 @@ export function useWebformSubmit(formId: string) {
return
}

let maybeCaptchaResponse = null

try {
maybeCaptchaResponse = getCaptchaResponse(captchaConfig, window)
} catch (e) {
console.error(e)

submissionState.value = {
status: 'error',
title: props.errorMessageTitle,
message: 'CAPTCHA ERROR',
receipt: ''
}

return
}

try {
const resData = await postForm(props.formId, data)
const resData = await postForm(props.formId, data, maybeCaptchaResponse)
console.log('asdklnalksd', resData)

const [code, note] = resData.attributes?.notes?.split('|') || []

Expand Down
68 changes: 68 additions & 0 deletions packages/ripple-tide-webform/composables/useCaptchaWidget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useRuntimeConfig, useNuxtData, useFetch, useTideError } from '#imports'

Check warning on line 1 in packages/ripple-tide-webform/composables/useCaptchaWidget.ts

View workflow job for this annotation

GitHub Actions / Test

'useRuntimeConfig' is defined but never used. Allowed unused vars must match /props/u

Check warning on line 1 in packages/ripple-tide-webform/composables/useCaptchaWidget.ts

View workflow job for this annotation

GitHub Actions / Test

'useNuxtData' is defined but never used. Allowed unused vars must match /props/u

Check warning on line 1 in packages/ripple-tide-webform/composables/useCaptchaWidget.ts

View workflow job for this annotation

GitHub Actions / Test

'useFetch' is defined but never used. Allowed unused vars must match /props/u

Check warning on line 1 in packages/ripple-tide-webform/composables/useCaptchaWidget.ts

View workflow job for this annotation

GitHub Actions / Test

'useTideError' is defined but never used. Allowed unused vars must match /props/u

const getThirdPartyScript = (captchaType: string) => {
switch (captchaType) {
case 'recaptcha':
return {
src: 'https://www.google.com/recaptcha/api.js?render=explicit',
tagPosition: 'head',
async: false,
defer: true
}
case 'turnstile':
return {
src: 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit',
tagPosition: 'head',
async: false,
defer: false
}
default:
return null
}
}

const initialiseCaptcha = (formId, captchaConfig, _window) => {
const captchaElementId = `rpl-captcha-element__${formId}`

switch (captchaConfig.type) {
case 'recaptcha':
if (_window) {
return _window.grecaptcha.render(captchaElementId, {
sitekey: captchaConfig.siteKey,
theme: 'light'
})
}
break
case 'turnstile':
if (_window) {
return _window.turnstile.ready(function () {
window.turnstile.render(`#${captchaElementId}`, {
sitekey: captchaConfig.siteKey,
callback: function (token) {
console.log(`Challenge Success ${token}`)
}
})
})
}
break
default:
return null
}
}

export const useCaptchaWidget = async (formId, captchaConfig): Promise<any> => {
console.log('asdlknaslkdasd', captchaConfig)
if (!captchaConfig?.enabled) {
return null
}

useHead({
script: [getThirdPartyScript(captchaConfig.type)]
})

onMounted(() => {
initialiseCaptcha(formId, captchaConfig, window)
})
}

export default useCaptchaWidget
1 change: 1 addition & 0 deletions packages/ripple-tide-webform/mapping/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { getFormSchemaFromMapping } from './webforms-mapping'
export { getCaptchaSettings } from './webforms-captcha-mapping'
10 changes: 10 additions & 0 deletions packages/ripple-tide-webform/mapping/webforms-captcha-mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ApiWebForm } from './../types'

export const getCaptchaSettings = (webform: ApiWebForm): CaptchaSettings => {
return {
enabled: webform?.third_party_settings?.tide_webform?.enable_captcha === 1,
type: webform?.third_party_settings?.tide_webform?.captcha_type,
siteKey:
webform?.third_party_settings?.tide_webform?.captcha_details?.site_key
}
}
6 changes: 4 additions & 2 deletions packages/ripple-tide-webform/server/api/tide/webform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
} from '@dpc-sdp/ripple-tide-api/types'
import { useRuntimeConfig } from '#imports'
import { AuthCookieNames } from '@dpc-sdp/nuxt-ripple-preview/utils'
import { getFormSchemaFromMapping } from '../../../mapping'
import { getFormSchemaFromMapping, getCaptchaSettings } from '../../../mapping'

/**
* @description Custom API call methods and response mapping for webform
Expand All @@ -30,7 +30,9 @@ class TideWebformApi extends TideApiBase {
_src: (field: any) =>
process.env.NODE_ENV === 'development' ? field : undefined,
schema: async (field: any, page, tidePageApi: TidePageApi) =>
await getFormSchemaFromMapping(field, tidePageApi)
await getFormSchemaFromMapping(field, tidePageApi),
captchaConfig: (field: any) =>
getCaptchaSettings(field.field_paragraph_webform)
},
includes: []
}
Expand Down
4 changes: 4 additions & 0 deletions packages/ripple-tide-webform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ export interface ApiField {
field_paragraph_title: string
field_paragraph_webform: ApiWebForm
}

export interface CaptchaSettings {
type: string
}
34 changes: 34 additions & 0 deletions packages/ripple-tide-webform/utils/captcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const getGoogleRecaptchaResponse = (_window) => {
return _window.grecaptcha.getResponse()
}

const getCloudfareTurnstileResponse = (_window) => {

Check warning on line 5 in packages/ripple-tide-webform/utils/captcha.ts

View workflow job for this annotation

GitHub Actions / Test

'_window' is defined but never used
// TODO
return ''
}

const getResponse = (captchaType, _window) => {
// TODO
switch (captchaType) {
case 'recaptcha':
return getGoogleRecaptchaResponse(_window)
case 'turnstile':
return getCloudfareTurnstileResponse(_window)
default:
return null
}
}

export const getCaptchaResponse = (captchaConfig, _window) => {
if (!captchaConfig.enabled) {
return null
}

const response = getResponse(captchaConfig.type, _window)

if (!response) {
throw new Error('No captcha response')
}

return response
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const handleReset = () => {
}
</script>
<template>
<div :id="`rpl-captcha-element__${form.id}`"></div>
<div class="rpl-form-actions rpl-u-screen-only">
<RplButton
:id="id"
Expand Down

0 comments on commit 4c4f517

Please sign in to comment.