From cab2d530c8e17c98ff7eb7d727be9174cafdd10d Mon Sep 17 00:00:00 2001 From: David Fridley Date: Wed, 3 Jun 2026 14:37:05 -0700 Subject: [PATCH 1/5] ignore machine generated files of webpack --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index efa180355..c3cd7a3c4 100644 --- a/.gitignore +++ b/.gitignore @@ -127,4 +127,7 @@ docker_environment_init.sh .envrc # Exclude Makefiles -Makefile \ No newline at end of file +Makefile + +# Exclude files create at runtime - by webpack and storybook +common/static/ \ No newline at end of file From d578511790be9931b14a296dcf1805cc62e772d5 Mon Sep 17 00:00:00 2001 From: David Fridley Date: Wed, 3 Jun 2026 18:00:05 -0700 Subject: [PATCH 2/5] Fix Yarn apt GPG key for bookworm-based Docker builds --- Dockerfile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Dockerfile b/Dockerfile index 4439a5983..941286b45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,13 @@ # https://hub.docker.com/r/nikolaik/python-nodejs FROM nikolaik/python-nodejs:python3.10-nodejs16 +# Refresh Yarn repo GPG key (old key 62D54FD4003F6525 expired; fetch the +# current one so apt-get update succeeds on bookworm-based images). +RUN curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg \ + | gpg --dearmor -o /usr/share/keyrings/yarn-archive-keyring.gpg \ + && sed -i 's|^deb \[.*\] https://dl.yarnpkg.com|deb [signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com|;s|^deb https://dl.yarnpkg.com|deb [signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com|' \ + /etc/apt/sources.list.d/yarn.list 2>/dev/null || true + # This to get GDAL thanks to https://stackoverflow.com/questions/62546706/how-do-i-install-gdal-in-a-python-docker-environment RUN apt-get update && apt-get install From 696fee3b3fc0189548c135b3739a1644a91efc98 Mon Sep 17 00:00:00 2001 From: David Fridley Date: Tue, 16 Jun 2026 11:47:49 -0700 Subject: [PATCH 3/5] Harden signup and Mailchimp subscription flow --- .../controllers/SignUpController.jsx | 30 +++++++ common/helpers/mailing_list.py | 12 ++- ...contributor_newsletter_signup_requested.py | 16 ++++ democracylab/models.py | 1 + democracylab/settings.py | 2 + democracylab/views.py | 79 +++++++++++++++++-- 6 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 democracylab/migrations/0011_contributor_newsletter_signup_requested.py diff --git a/common/components/controllers/SignUpController.jsx b/common/components/controllers/SignUpController.jsx index 3ea63e636..15e367d16 100644 --- a/common/components/controllers/SignUpController.jsx +++ b/common/components/controllers/SignUpController.jsx @@ -2,6 +2,7 @@ import DjangoCSRFToken from "django-react-csrftoken"; import React from "react"; +import ReCAPTCHA from "react-google-recaptcha"; import type { Validator } from "../forms/FormValidation.jsx"; import FormValidation from "../forms/FormValidation.jsx"; import metrics from "../utils/metrics.js"; @@ -27,6 +28,7 @@ type State = {| validations: $ReadOnlyArray, termsOpen: boolean, didCheckTerms: boolean, + reCaptchaValue: ?string, isValid: boolean, |}; @@ -50,6 +52,7 @@ class SignUpController extends React.Component { password2: "", termsOpen: false, didCheckTerms: false, + reCaptchaValue: null, isValid: false, validations: [ { @@ -88,10 +91,23 @@ class SignUpController extends React.Component { checkFunc: (state: State) => state.didCheckTerms, errorMessage: "Agree to terms of service", }, + { + checkFunc: (state: State) => + !this.isCaptchaEnabled() || !_.isEmpty(state.reCaptchaValue), + errorMessage: "Please complete the captcha", + }, ], }; } + isCaptchaEnabled(): boolean { + return !_.isEmpty(window.GR_SITEKEY); + } + + reCaptchaOnChange(value: ?string): void { + this.setState({ reCaptchaValue: value }); + } + onValidationCheck(isValid: boolean): void { if (isValid !== this.state.isValid) { this.setState({ isValid }); @@ -153,6 +169,11 @@ class SignUpController extends React.Component { /> +
@@ -206,6 +227,15 @@ class SignUpController extends React.Component { formState={this.state} /> + {this.isCaptchaEnabled() ? ( +
+ +
+ ) : null} +