Skip to content

Commit

Permalink
Migrate captcha to cloudflare (#314)
Browse files Browse the repository at this point in the history
* Migrate captcha to cloudflare

* Fix headers
  • Loading branch information
sonic16x authored Nov 18, 2024
1 parent b824ae7 commit 9bcc253
Show file tree
Hide file tree
Showing 10 changed files with 58 additions and 72 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
"lint-fix": "eslint src/**/*.{ts,tsx} --fix",
"generateDictionary": "ts-node -r tsconfig-paths/register ./src/services/generateDictionary.ts",
"build": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js",
"build-api": "webpack --config webpack.config.api.js",
"deploy-api": "wrangler publish"
"deploy-api": "wrangler deploy ./src/server/index.ts --name api --compatibility-date 2024-11-12"
},
"jest": {
"transform": {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Modals/WordErrorModal/WordErrorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const WordErrorModal = () => {

useEffect(() => {
if (online) {
grecaptcha.render('recaptcha-element', {
grecaptcha.render('#recaptcha-element', {
sitekey: captchaSiteKey,
theme: 'light',
callback: (token) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ResultsCard/ResultsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export const ResultsCard =
{short ? <ShareIcon /> : t('shareWord')}
</button>
<button
className="results-card__action-button hide"
className="results-card__action-button"
type="button"
aria-label={t('reportWordError')}
onClick={showWordErrorModal}
Expand Down
2 changes: 1 addition & 1 deletion src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,6 @@ export const wordErrorsTypes = [
'interslavic',
]

export const captchaSiteKey = '6Lccu5kdAAAAACvN1Cnl5THInIZPEmqIyJOMjkpe';
export const captchaSiteKey = '0x4AAAAAAAz3DWhwL4ABSW4W';
export const wordErrorTextMaxLength = 120;
export const REP_LINK = 'https://github.com/sonic16x/interslavic'
4 changes: 0 additions & 4 deletions src/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,5 @@ declare module '*.svg' {
export default content;
}

declare const GOOGLE_CAPTCHA_SECRET_KEY: string;
declare const GOOGLE_WORD_ERRORS_TABLE_ID: string;
declare const GOOGLE_SERVICE_ACCOUNT_EMAIL: string;
declare const GOOGLE_PRIVATE_KEY: string;
declare const FB: any;
declare const IS_COM: boolean;
3 changes: 1 addition & 2 deletions src/index.html.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
ga('send', 'pageview');
</script>
<% } %>
<script src="https://www.google.com/recaptcha/api.js?render=explicit" async defer>
</script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?compat=recaptcha" async defer></script>
<script src="is_com.js"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
Expand Down
15 changes: 6 additions & 9 deletions src/server/checkCaptcha.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { toQueryString } from 'utils/toQueryString';

export async function checkCaptcha(secret, response) {
const captchaParams = toQueryString({
secret,
response,
});

const captchaResponse = await fetch(`https://www.google.com/recaptcha/api/siteverify?${captchaParams}`, {
export async function checkCaptcha(secret, response, remoteip) {
const captchaResponse = await fetch(`https://challenges.cloudflare.com/turnstile/v0/siteverify`, {
method: 'POST',
body: JSON.stringify({ secret, response, remoteip }),
headers: {
'Content-Type': 'application/json'
}
});

const captchaResponseData = await captchaResponse.json();
Expand Down
4 changes: 2 additions & 2 deletions src/server/googleAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ export async function googleAuth(googleServiceAccountEmail, googlePrivateKey) {
});

// Grab the JSON from the response
const { access_token } = await response.json();
const res: any = await response.json();

// Capture the access token
return access_token;
return res.access_token;
}
63 changes: 45 additions & 18 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,15 @@ import { getTableHeader } from 'server/getTableHeader';
import { googleAuth } from 'server/googleAuth';
import { validateData } from 'server/validateData';

addEventListener('fetch', (event) => {
// eslint-disable-next-line
// @ts-ignore
event.respondWith(
// eslint-disable-next-line
// @ts-ignore
handleRequest(event.request).catch(
(err) => new Response(err.stack, { status: 500 })
)
);
});

const responseHeaders = {
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
'Access-Control-Max-Age': '86400',
}

const responseHeaders = {
...corsHeaders,
'Content-Type': 'application/json',
}

Expand All @@ -30,7 +23,32 @@ function responseError(error) {
});
}

async function handleRequest(request) {
function handleOptions (request) {
if (
request.headers.get("Origin") !== null &&
request.headers.get("Access-Control-Request-Method") !== null &&
request.headers.get("Access-Control-Request-Headers") !== null
) {
return new Response(null, {
headers: {
...corsHeaders,
"Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers"),
},
})
} else {
return new Response(null, {
headers: {
Allow: "GET, HEAD, POST, OPTIONS",
},
})
}
}

async function handleRequest(request, env) {
if (request.method === "OPTIONS") {
return handleOptions(request)
}

const data = await request.json();
const { pathname } = new URL(request.url);

Expand All @@ -44,19 +62,20 @@ async function handleRequest(request) {
return responseError('invalidData');
}

const captchaIsOk = await checkCaptcha(GOOGLE_CAPTCHA_SECRET_KEY, data.captchaToken);
const ip = request.headers.get('CF-Connecting-IP');
const captchaIsOk = await checkCaptcha(env.GOOGLE_CAPTCHA_SECRET_KEY, data.captchaToken, ip);

if (!captchaIsOk) {
return responseError('invalidCaptcha');
}

const googleAccessToken = await googleAuth(GOOGLE_SERVICE_ACCOUNT_EMAIL, GOOGLE_PRIVATE_KEY);
const googleAccessToken = await googleAuth(env.GOOGLE_SERVICE_ACCOUNT_EMAIL, env.GOOGLE_PRIVATE_KEY);

if (!googleAccessToken) {
return responseError('invalidGoogleAccessToken');
}

const tableHeader = await getTableHeader(GOOGLE_WORD_ERRORS_TABLE_ID, googleAccessToken);
const tableHeader = await getTableHeader(env.GOOGLE_WORD_ERRORS_TABLE_ID, googleAccessToken);

if (!tableHeader || !tableHeader.length) {
return responseError('invalidTableHeader');
Expand All @@ -70,14 +89,22 @@ async function handleRequest(request) {
return data[fieldName];
});

const addRowResponse = await addRow(GOOGLE_WORD_ERRORS_TABLE_ID, newRow, googleAccessToken);
const addRowResponse = await addRow(env.GOOGLE_WORD_ERRORS_TABLE_ID, newRow, googleAccessToken);

if (addRowResponse.status === 200) {
return new Response(JSON.stringify({}), {
return new Response(JSON.stringify({ error: null }), {
status: addRowResponse.status,
headers: responseHeaders,
});
} else {
return responseError('invalidAddRow');
}
}

export default {
async fetch(request, env) {
return handleRequest(request, env).catch(
(err) => responseError(err.stack)
)
}
}
32 changes: 0 additions & 32 deletions webpack.config.api.js

This file was deleted.

0 comments on commit 9bcc253

Please sign in to comment.