Skip to content

Commit

Permalink
add captcha
Browse files Browse the repository at this point in the history
  • Loading branch information
jakehobbs committed Jan 21, 2024
1 parent bce0425 commit 6cefb75
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 7 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"lucide-react": "^0.309.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-google-recaptcha": "^3.1.0",
"react-hook-form": "^7.49.3",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
Expand All @@ -38,6 +39,7 @@
"@types/node": "^20.11.2",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@types/react-google-recaptcha": "^2.1.8",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react-swc": "^3.5.0",
Expand Down
50 changes: 50 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions service/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import (
)

var (
Port = getEnvWithFallback("PORT", "3333")
Dsn = getEnvWithFallback("DB_CONNECTION_STRING", "adb_user:adbpassword@tcp(localhost:3306)/campaign_mailer")
Port = getEnvWithFallback("PORT", "3333")
Dsn = getEnvWithFallback("DB_CONNECTION_STRING", "adb_user:adbpassword@tcp(localhost:3306)/campaign_mailer")
RecaptchaSecret = getEnvWithFallback("RECAPTCHA_SECRET", "")
)

func getEnvWithFallback(key string, fallback string) string {
Expand Down
18 changes: 17 additions & 1 deletion service/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"github.com/dxe/helptheducks.com/service/config"
"github.com/dxe/helptheducks.com/service/model"
"net/http"
"net/mail"
Expand All @@ -17,7 +18,7 @@ type CreateMessageInput struct {
Zip string `json:"zip,omitempty"`
City string `json:"city,omitempty"`
Message string `json:"message"`
// TODO: add captcha.
Token string `json:"token"`
}

func createMessageHandler(w http.ResponseWriter, r *http.Request) {
Expand All @@ -29,6 +30,21 @@ func createMessageHandler(w http.ResponseWriter, r *http.Request) {
return
}

if config.RecaptchaSecret == "" {
fmt.Println("Recaptcha secret not set, skipping verification")
} else {
ok, err := verifyRecaptcha(body.Token)
if err != nil {
fmt.Printf("error verifying recaptcha: %v\n", err)
http.Error(w, "error verifying recaptcha", http.StatusInternalServerError)
return
}
if !ok {
http.Error(w, "invalid captcha", http.StatusForbidden)
return
}
}

_, err = mail.ParseAddress(body.Email)
if err != nil {
http.Error(w, fmt.Sprintf("invalid email address: %v", err), http.StatusBadRequest)
Expand Down
30 changes: 30 additions & 0 deletions service/recaptcha.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"encoding/json"
"github.com/dxe/helptheducks.com/service/config"
"net/http"
"net/url"
)

type RecaptchaResponse struct {
Success bool `json:"success"`
}

const verifyUrl = "https://www.google.com/recaptcha/api/siteverify"

func verifyRecaptcha(token string) (bool, error) {
resp, err := http.PostForm(verifyUrl, url.Values{
"secret": {config.RecaptchaSecret},
"response": {token},
})
if err != nil {
return false, err
}
defer resp.Body.Close()
var result RecaptchaResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, err
}
return result.Success, nil
}
29 changes: 26 additions & 3 deletions src/components/petition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "../data/petition.ts";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Form,
FormControl,
Expand All @@ -31,17 +31,20 @@ import {
import ky from "ky";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert.tsx";
import { LoaderIcon, MailCheckIcon } from "lucide-react";
import ReCAPTCHA from "react-google-recaptcha";

const PETITION_API_URL = "https://petitions-229503.appspot.com/api/sign";

const CAMPAIGN_MAILER_API_URL = "https://helptheducks.dxe.io/message/create";
const CAMPAIGN_MAILER_API_URL = `${import.meta.env.PROD ? "https://helptheducks.dxe.io" : "http://localhost:3333"}/message/create`;

declare global {
interface Window {
dataLayer?: unknown[];
}
}

const CAPTCHA_SITE_KEY = "6LdiglcpAAAAAM9XE_TNnAiZ22NR9nSRxHMOFn8E";

export const Petition = () => {
const form = useForm<PetitionForm>({
resolver: zodResolver(PetitionFormSchema),
Expand Down Expand Up @@ -70,10 +73,21 @@ export const Petition = () => {
const onSubmit = useMemo(
() =>
handleSubmit(async (data) => {
setIsSubmitting(true);
window.dataLayer?.push({
event: "form_submitted",
});
setIsSubmitting(true);
if (!recaptchaRef.current) {
alert("Error loading captcha. Please refresh the page & try again.");
setIsSubmitting(false);
return;
}
const token = await recaptchaRef.current.executeAsync();
if (!token) {
alert("Captcha error. Please refresh the page & try again.");
setIsSubmitting(false);
return;
}
// We purposefully do these one at a time. If the first one fails,
// we don't want to submit the second one. This allows the user to
// resubmit the form without causing duplicate emails to be sent.
Expand Down Expand Up @@ -107,6 +121,7 @@ export const Petition = () => {
...(data.zip && { zip: data.zip }),
...(data.city && { city: data.city }),
message: data.message,
token,
},
headers: {
"Content-Type": "application/json",
Expand Down Expand Up @@ -163,6 +178,8 @@ export const Petition = () => {
[dirtyFields.message, resetField],
);

const recaptchaRef = useRef<ReCAPTCHA>(null);

return isSubmitted ? (
<Alert className="self-center w-fit bg-slate-100">
<MailCheckIcon className="h-4 w-4" />
Expand Down Expand Up @@ -335,6 +352,12 @@ export const Petition = () => {
)}
Submit
</Button>
<ReCAPTCHA
ref={recaptchaRef}
sitekey={CAPTCHA_SITE_KEY}
badge="bottomright"
size="invisible"
/>
<p className="text-xs text-center">
By signing, you agree to receive email messages from Direct Action
Everywhere. You may unsubscribe at any time.
Expand Down
1 change: 0 additions & 1 deletion src/components/svg/play-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
export const PlayIcon = ({ className }: { className?: string }) => (
<svg
className={className}
// height="32px"
version="1.1"
viewBox="0 0 32 32"
xmlSpace="preserve"
Expand Down

0 comments on commit 6cefb75

Please sign in to comment.