Last active
September 28, 2022 22:26
-
-
Save bcnzer/5911b9375df24489ad327fecf6e4878c to your computer and use it in GitHub Desktop.
Example of a Cloudflare Worker handling Google reCAPTCHA in an "edged out" POST request
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// NOTE: all auth code has been removed for the sake of brevity. Please see part 2 of blog series | |
// for more info: https://liftcodeplay.com/2018/10/16/pushing-my-api-to-the-edge-part-2-authentication-and-authorization/ | |
addEventListener('fetch', event => { | |
event.respondWith(handleRequest(event)) | |
}) | |
const genderFemale = 'Female' | |
const genderMale = 'Male' | |
/** | |
* Double-check all the fields that got sent | |
*/ | |
function validateEntryForm(body) { | |
if (!body) return false | |
if (!body.firstName || !body.lastName || !body.dateOfBirth || !body.nominatedDivision || !body.nominatedWeightClass) return false | |
const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ | |
if (!body.emailAddress || !emailRegex.test(body.emailAddress)) return false | |
const mobileNumberRegex = /^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$/ | |
if (!body.mobileNumber || !mobileNumberRegex.test(body.mobileNumber)) return false | |
console.log(body.gender) | |
if (!body.gender || !(body.gender === genderMale || body.gender === genderFemale)) return false | |
const pbRegex = /^\d+\.?\d*$/ | |
if (!!body.personalBestSquat && !pbRegex.test(body.personalBestSquat)) return false | |
if (!!body.personalBestBench && !pbRegex.test(body.personalBestBench)) return false | |
if (!!body.personalBestDeadlift && !pbRegex.test(body.personalBestDeadlift)) return false | |
return true | |
} | |
/** | |
* Entry point of the worker | |
*/ | |
async function handleRequest(event) { | |
// Generate the CORS headers I'll have to return with requests | |
const corsHeaders = setCorsHeaders(new Headers()) | |
try { | |
const requestMethod = event.request.method | |
const requestUrl = new URL(event.request.url) | |
console.log(requestUrl) | |
// Always return the same CORS info | |
if(requestMethod === 'OPTIONS') { | |
return new Response('', { headers:corsHeaders }) | |
} | |
// POST the ENTRY DATA to the queue but first check the reCAPTCHA | |
if (requestMethod === 'POST' && requestUrl.hostname === 'api.powerlifting.com' && requestUrl.pathname === '/v1/entry') { | |
console.log(event.request) | |
const requestBody = await event.request.json() | |
if (!validateEntryForm(requestBody)) { | |
return new Response('Invalid body', { status: 400, headers:corsHeaders }) | |
} | |
const recaptchaToken = event.request.headers.get('g-recaptcha') | |
if (!recaptchaToken) { | |
return new Response('Invalid reCAPTCHA', { status: 400, headers:corsHeaders }) | |
} | |
// My reCAPTCHA secret is stored in KV. KVs are encrypted at rest and in transit | |
const recaptchaSecret = await SIGNUP_DATA.get('recaptchasecret') | |
const recaptchaResponse = await fetch( | |
`https://www.google.com/recaptcha/api/siteverify?secret=${recaptchaSecret}&response=${recaptchaToken}`, { | |
method: 'POST' | |
}) | |
const recaptchaBody = await recaptchaResponse.json() | |
if (recaptchaBody.success == true) { | |
/* TODO - Send it onto a queue - make a REST API call. Could be something like this: | |
await fetch( | |
`https://myqueueurl.com/${someValue}`, { | |
method: 'POST', | |
body: requestBody | |
}) | |
*/ | |
return new Response('', { status: 202, headers:corsHeaders }) | |
} else { | |
return new Response('reCAPTCHA failed', { status: 400, headers:corsHeaders }) | |
} | |
} | |
// GET the competition info from KV | |
const entryRegex = /^\/v1\/entry\/\d+$/ // i.e. /v1/entry/17043 | |
if (requestMethod === 'GET' && requestUrl.hostname === 'api.powerlifting.com' && entryRegex.test(requestUrl.pathname)) { | |
const entryCompId = requestUrl.pathname.split('/')[3] | |
console.log(entryCompId) | |
const entryData = await SIGNUP_DATA.get(entryCompId) | |
return new Response(entryData, { status: 200, headers:corsHeaders }) | |
} | |
// Get ALL the WEIGHT CLASSES and DIVISIONS from KV and return them | |
if (requestMethod === 'GET' && requestUrl.hostname === 'api.powerlifting.com' && requestUrl.pathname === '/v1/entry/weightclassanddivisions') { | |
return getWeightClassAndDivisions(corsHeaders) | |
} | |
// Get the DIVISIONS from KV and return them | |
if (requestMethod === 'GET' && requestUrl.hostname === 'api.powerlifting.com' && requestUrl.pathname === '/v1/entry/divisions') { | |
return getDivisions(requestUrl, corsHeaders) | |
} | |
// Get the WEIGHT CLASSES from KV and return them | |
if (requestMethod === 'GET' && requestUrl.hostname === 'api.powerlifting.com' && requestUrl.pathname === '/v1/entry/weightclasses') { | |
return getWeightClasses(requestUrl, corsHeaders) | |
} | |
const response = await fetch(event.request) | |
return response | |
} | |
catch (err) { | |
console.error(err) | |
return new Response(err.stack, { status: 500, headers:corsHeaders }) | |
} | |
} | |
function setCorsHeaders(headers) { | |
headers.set('Access-Control-Allow-Origin', '*') | |
headers.set('Access-Control-Allow-Methods', 'POST, GET') | |
headers.set('Access-Control-Allow-Headers', 'access-control-allow-headers, g-recaptcha') | |
headers.set('Access-Control-Max-Age', 1728000) | |
return headers | |
} | |
/** | |
* Get the static DIVISIONS from KV and return them | |
*/ | |
async function getDivisions(requestUrl, corsHeaders) { | |
const age = requestUrl.searchParams.get('age') | |
const gender = requestUrl.searchParams.get('gender') | |
if (!!gender && gender !== genderFemale && gender !== genderMale) { | |
return new Response('', { status: 400, headers:corsHeaders }) | |
} | |
const storedDivisions = await SIGNUP_DATA.get('ipf2018') | |
let divisions = JSON.parse(storedDivisions).divisions | |
if (!!age || !!gender) { | |
divisions = divisions.filter(function(div) { | |
if (!!age && !!gender) { | |
// If you specify an age you must specify a gender | |
return age >= div.startAge && age <= div.endAge && gender === div.gender | |
} else if (!age && !!gender) { | |
// It's fine if you just specify a gender | |
return gender === div.gender | |
} else if (!!age && !gender) { | |
// Not ok if you specify an age but no gender | |
return false | |
} | |
// Return everything | |
return true | |
}) | |
} | |
divisions = divisions.map(function(div) { | |
return { | |
"gender": div.gender, | |
'name': div.name, | |
'abbrev': div.abbrev | |
} | |
}) | |
console.log(divisions) | |
return new Response(JSON.stringify(divisions), { status: 200, headers:corsHeaders }) | |
} | |
/** | |
* Get the static WEIGHT CLASSES from KV and return them | |
*/ | |
async function getWeightClasses(requestUrl, corsHeaders) { | |
const weight = requestUrl.searchParams.get('weight') | |
const gender = requestUrl.searchParams.get('gender') | |
if (!!gender && gender !== genderFemale && gender !== genderMale) { | |
return new Response('', { status: 400, headers:corsHeaders }) | |
} | |
const storedWeightClasses = await SIGNUP_DATA.get('ipf2018') | |
let weightClasses = JSON.parse(storedWeightClasses).weightClasses | |
if (!!weight) { | |
weightClasses = weightClasses.filter(function(wc) { | |
return weight >= wc.startWeight && weight <= wc.endWeight | |
}) | |
} | |
if (!!weight || !!gender) { | |
weightClasses = weightClasses.filter(function(wc) { | |
if (!!weight && !!gender) { | |
// If you specify an weight you must specify a gender | |
return weight >= wc.startWeight && weight <= wc.endWeight && gender === wc.gender | |
} else if (!weight && !!gender) { | |
// It's fine if you just specify a gender | |
return gender === wc.gender | |
} else if (!!weight && !gender) { | |
// Not ok if you specify a weight but no gender | |
return false | |
} | |
// Return everything | |
return true | |
}) | |
} | |
weightClasses = weightClasses.map(function(wc) { | |
return { | |
"gender": wc.gender, | |
'name': wc.name | |
} | |
}) | |
console.log(weightClasses) | |
return new Response(JSON.stringify(weightClasses), { status: 200, headers:corsHeaders }) | |
} | |
/** | |
* Get all the weight classes and divisions, for both headers, and return it as one data set | |
*/ | |
async function getWeightClassAndDivisions(corsHeaders) { | |
let storeData = await SIGNUP_DATA.get('ipf2018') | |
return new Response(storeData, { status: 200, headers:corsHeaders }) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment