Skip to content

Commit

Permalink
Added cloudflare turnstile
Browse files Browse the repository at this point in the history
  • Loading branch information
ravirajput10 committed Sep 24, 2024
1 parent 5aa40f1 commit feca96d
Show file tree
Hide file tree
Showing 19 changed files with 296 additions and 2 deletions.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
13 changes: 13 additions & 0 deletions apps/web/app/api/cloudflare/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NextRequest } from "next/server";
import { validateTurnstileToken } from "@courselit/utils";

export async function POST(req: NextRequest) {
const body = await req.json();
const { token } = body;

const verifyTurnstileToken = await validateTurnstileToken(token);
if (verifyTurnstileToken) {
return Response.json({ success: true });
}
return Response.json({ success: false }, { status: 403 });
}
8 changes: 8 additions & 0 deletions apps/web/app/api/config/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { NextRequest } from "next/server";

export async function GET(req: NextRequest) {
return Response.json(
{ turnstileSiteKey: process.env.TURNSTILE_SITE_KEY },
{ status: 200 },
);
}
3 changes: 3 additions & 0 deletions apps/web/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ MyApp.getInitialProps = wrapper.getInitialAppProps(
const backend = getBackendAddress(ctx.req.headers);
store.dispatch(actionCreators.updateBackend(backend));
try {
await (store.dispatch as ThunkDispatch<State, void, AnyAction>)(
actionCreators.updateConfig(),
);
await (store.dispatch as ThunkDispatch<State, void, AnyAction>)(
actionCreators.updateSiteInfo(),
);
Expand Down
1 change: 1 addition & 0 deletions packages/common-models/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ export type { Domain } from "./domain";
export type { Progress } from "./progress";
export type { Drip, DripType } from "./drip";
export type { PaymentMethod } from "./payment-method";
export type { ServerConfig } from "./server-config";
3 changes: 3 additions & 0 deletions packages/common-models/src/server-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ServerConfig {
turnstileSiteKey: string;
}
2 changes: 2 additions & 0 deletions packages/common-models/src/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Profile from "../profile";
import SiteInfo from "../site-info";
import Theme from "../theme";
import { Typeface } from "../typeface";
import { ServerConfig } from "../server-config";

export default interface State {
auth: Auth;
Expand All @@ -13,4 +14,5 @@ export default interface State {
address: Address;
theme: Theme;
typefaces: Typeface[];
config: ServerConfig;
}
1 change: 1 addition & 0 deletions packages/common-widgets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"react-dom": "*"
},
"devDependencies": {
"@types/next": "^9.0.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.57.0",
"eslint-plugin-n": "^15.2.0",
Expand Down
55 changes: 53 additions & 2 deletions packages/common-widgets/src/email-form/widget.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { FormEvent, useState } from "react";
import React, { FormEvent, useState, useEffect } from "react";
import { AppMessage } from "@courselit/common-models";
import Settings from "./settings";
import { actionCreators } from "@courselit/state-management";
Expand All @@ -18,6 +18,7 @@ import {
verticalPadding as defaultVerticalPadding,
horizontalPadding as defaultHorizontalPadding,
} from "./defaults";
import Script from "next/script";

export interface WidgetProps {
settings: Settings;
Expand Down Expand Up @@ -46,16 +47,43 @@ const Widget = ({
}: WidgetProps) => {
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [turnstileToken, setTurnstileToken] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string>("");

const justifyContent =
alignment === "center"
? "center"
: alignment === "right"
? "flex-end"
: "flex-start";

useEffect(() => {
if (state.config.turnstileSiteKey) {
(window as any).turnstileCallback = async (token: string) => {
setTurnstileToken(token);
};
}
}, [state.config.turnstileSiteKey]);

const onSubmit = async (e: FormEvent) => {
e.preventDefault();

if (state.config.turnstileSiteKey) {
const payload = JSON.stringify({ token: turnstileToken });
const verificationFetch = new FetchBuilder()
.setUrl(`${state.address.backend}/api/cloudflare`)
.setHeaders({
"Content-Type": "application/json",
})
.setPayload(payload)
.build();
const response = await verificationFetch.exec();
if (!response.success) {
setErrorMessage("Could not verify that you are a human.");
return;
}
}

const mutation = `
mutation {
response: createSubscription(name: "${name}" email: "${email}")
Expand Down Expand Up @@ -116,6 +144,9 @@ const Widget = ({
>
<h2 className="text-4xl mb-4">{title || DEFAULT_TITLE}</h2>
{subtitle && <h3 className="mb-4">{subtitle}</h3>}
{errorMessage && (
<div className="my-1 text-red-600">{errorMessage}</div>
)}
<div
className="flex flex-col md:!flex-row gap-2 w-full"
style={{
Expand Down Expand Up @@ -152,12 +183,32 @@ const Widget = ({
},
]}
/>
{state.config.turnstileSiteKey && (
<>
<Script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
async={true}
defer={true}
id="cloudflare-turnstile"
></Script>
<span
className="cf-turnstile"
data-sitekey={state.config.turnstileSiteKey}
data-callback="turnstileCallback"
/>
</>
)}

<Button2
style={{
backgroundColor: btnBackgroundColor,
color: btnForegroundColor,
}}
disabled={state.networkAction}
disabled={
state.networkAction ||
(state.config.turnstileSiteKey &&
!turnstileToken)
}
type="submit"
>
{btnText || DEFAULT_BTN_TEXT}
Expand Down
29 changes: 29 additions & 0 deletions packages/state-management/src/action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
THEME_AVAILABLE,
SET_ADDRESS,
TYPEFACES_AVAILABLE,
CONFIG_AVAILABLE,
} from "./action-types";
import { FetchBuilder } from "@courselit/utils";
import getAddress from "./utils/get-address";
Expand All @@ -27,6 +28,7 @@ import type {
import { AppMessage } from "@courselit/common-models";
import { ThunkAction } from "redux-thunk";
import { AnyAction } from "redux";
import { ServerConfig } from "@courselit/common-models";

export function signedIn() {
return async (dispatch: any) => {
Expand Down Expand Up @@ -168,6 +170,33 @@ export function updateSiteInfo(): ThunkAction<void, State, unknown, AnyAction> {
};
}

export function updateConfig(): ThunkAction<void, State, unknown, AnyAction> {
return async (dispatch: any, getState: () => State) => {
try {
dispatch(networkAction(true));

const fetch = new FetchBuilder()
.setUrl(`${getState().address.backend}/api/config`)
.setHttpMethod("GET")
.setIsGraphQLEndpoint(false)
.build();
const response = await fetch.exec();

if (response) {
dispatch(newConfigAvailable(response));
}
} catch (err) {
console.error(err); // eslint-disable-line no-console
} finally {
dispatch(networkAction(false));
}
};
}

export function newConfigAvailable(config: ServerConfig) {
return { type: CONFIG_AVAILABLE, config };
}

export function newSiteInfoAvailable(info: SiteInfo) {
return { type: SITEINFO_AVAILABLE, siteinfo: info };
}
Expand Down
1 change: 1 addition & 0 deletions packages/state-management/src/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export const SET_ADDRESS = "SET_ADDRESS";
export const WIDGETS_DATA_AVAILABLE = "WIDGETS_DATA_AVAILABLE";
export const FEATURE_FLAGS_AVAILABLE = "FEATURE_FLAGS_AVAILABLE";
export const TYPEFACES_AVAILABLE = "TYPEFACES_AVAILABLE";
export const CONFIG_AVAILABLE = "CONFIG_AVAILABLE";
1 change: 1 addition & 0 deletions packages/state-management/src/default-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ export default {
frontend: "",
},
typefaces: [],
config: { turnstileSiteKey: "" },
};
11 changes: 11 additions & 0 deletions packages/state-management/src/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
THEME_AVAILABLE,
SET_ADDRESS,
TYPEFACES_AVAILABLE,
CONFIG_AVAILABLE,
} from "./action-types";
import { HYDRATE } from "next-redux-wrapper";
import initialState from "./default-state";
Expand Down Expand Up @@ -167,6 +168,15 @@ function typefacesReducer(state = initialState.typefaces, action: Action) {
}
}

function configReducer(state = initialState.config, action: Action) {
switch (action.type) {
case CONFIG_AVAILABLE:
return action.config;
default:
return state;
}
}

const appReducers = combineReducers({
auth: authReducer,
siteinfo: siteinfoReducer,
Expand All @@ -176,6 +186,7 @@ const appReducers = combineReducers({
theme: themeReducer,
address: addressReducer,
typefaces: typefacesReducer,
config: configReducer,
//widgetsData: widgetsDataReducer,
});

Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { default as getGraphQLQueryStringFromObject } from "./get-graphql-query-
export { default as convertFiltersToDBConditions } from "./convert-filters-to-db-conditions";
export { default as slugify } from "@sindresorhus/slugify";
export { default as renderEmailContent } from "./render-email-content";
export { default as validateTurnstileToken } from "./validate-turnstile-token";
31 changes: 31 additions & 0 deletions packages/utils/src/validate-turnstile-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const SECRET_KEY = process.env.TURNSTILE_SECRET_KEY;

export default async function validateTurnstileToken(token: string) {
if (!SECRET_KEY) {
throw new Error("Turnstile: No secret key found");
}

const formData = new FormData();
formData.append("secret", SECRET_KEY || "");
formData.append("response", token || "");

const url = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const result = await fetch(url, {
body: formData,
method: "POST",
});

const outcome = await result.json();
if (outcome.success) {
return true;
}
if (
outcome["error-codes"] &&
outcome["error-codes"][0] === "timeout-or-duplicate"
) {
return true;
}
// eslint-disable-next-line no-console
console.log(`Turnstile validation failed`, outcome);
return false;
}
Loading

0 comments on commit feca96d

Please sign in to comment.