Skip to content

Commit 28ee84c

Browse files
nofurtherinformationclaudefangge518
authored
Turnstile migration (#663)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: fangge518 <fangge518@gmail.com>
1 parent 890dc06 commit 28ee84c

36 files changed

Lines changed: 419 additions & 322 deletions

.agents/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Agents should read the relevant guide(s) before making changes:
114114
- [`learn-map-lifecycle`](./skills/project/learn-map-lifecycle/SKILL.md) - map data lifecycle: imports, shatter setup, edges, graph linkage
115115
- [`learn-pipelines`](./skills/project/learn-pipelines/SKILL.md) - tiles/tabular/transforms pipeline contracts and toolchain requirements
116116
- [`learn-cms-moderation`](./skills/project/learn-cms-moderation/SKILL.md) - CMS editing/review and moderation workflows
117-
- [`learn-auth-share`](./skills/project/learn-auth-share/SKILL.md) - Auth0 scopes, recaptcha, and share/edit token security
117+
- [`learn-auth-share`](./skills/project/learn-auth-share/SKILL.md) - Auth0 scopes, Turnstile captcha, and share/edit token security
118118

119119
### Guide Selection Rules
120120

.agents/ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ IndexedDB serves as offline cache and conflict resolution source. Debounced writ
128128
- **Bulk assignments**: `PUT /api/assignments` uses PostgreSQL COPY for performance with optimistic concurrency
129129
- **Shatter operations**: `PATCH /api/assignments/{doc_id}/shatter` handles parent → child decomposition
130130
- **Contiguity**: Graph-based checking via NetworkX
131-
- **Auth**: Auth0 JWT with scopes (default/editor/admin), reCAPTCHA for public forms
131+
- **Auth**: Auth0 JWT with scopes (default/editor/admin), Cloudflare Turnstile for public forms
132132

133133
### Database Design
134134

.agents/skills/project/learn-auth-share/SKILL.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
---
22
name: learn-auth-share
3-
description: Auth0 scopes, recaptcha verification, and share/edit token security patterns
3+
description: Auth0 scopes, Turnstile captcha verification, and share/edit token security patterns
44
user-invocable: false
55
---
66

77
# Auth & Share Security
88

9-
Authentication/authorization and share-security behavior across Auth0 scopes, recaptcha verification, and map share/edit-access tokens.
9+
Authentication/authorization and share-security behavior across Auth0 scopes, Turnstile captcha verification, and map share/edit-access tokens.
1010

1111
## When To Use
1212
- You are changing protected endpoints, scopes, or role behavior.
@@ -26,7 +26,7 @@ Authentication/authorization and share-security behavior across Auth0 scopes, re
2626
## Hard Invariants
2727
- Protected backend routes must enforce required scopes via `auth.verify`.
2828
- Auth0 token validation must preserve audience/issuer/algorithm constraints.
29-
- Recaptcha-gated flows must verify token server-side.
29+
- Captcha-gated flows must verify token server-side.
3030
- Share-token payload and password behavior must remain compatible with frontend share/edit flows.
3131
- Public document access must not expose private document identifiers.
3232

@@ -38,14 +38,14 @@ Authentication/authorization and share-security behavior across Auth0 scopes, re
3838

3939
## Anti-Patterns
4040
- Broadening scopes or bypassing route security checks for convenience.
41-
- Performing recaptcha checks only in frontend.
41+
- Performing captcha checks only in frontend.
4242
- Logging sensitive token/password data.
4343
- Returning internal IDs/secrets in public route responses.
4444

4545
## Change Checklist
4646
1. Verify scope enforcement on all touched protected routes.
4747
2. Verify token verification config (audience/issuer/algorithms) is unchanged or intentionally updated.
48-
3. Verify recaptcha validation still runs for relevant form endpoints.
48+
3. Verify captcha validation still runs for relevant form endpoints.
4949
4. Verify share/password flow works for read/edit transitions.
5050
5. Verify no sensitive data is exposed in logs/responses.
5151

@@ -57,4 +57,4 @@ Authentication/authorization and share-security behavior across Auth0 scopes, re
5757
- 401/403 regressions from missing scopes in frontend-acquired tokens.
5858
- Broken admin pages from session/token wiring drift.
5959
- Password-protected map edit failures due to token/payload mismatch.
60-
- False recaptcha failures from env/config mistakes.
60+
- False captcha failures from env/config mistakes.

.github/workflows/deploy-app.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ jobs:
5454
working-directory: app
5555
run: |
5656
cat <<'EOF' > .env.production
57-
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=${{ secrets.RECAPTCHA_SITE_KEY }}
58-
NEXT_PUBLIC_RECAPTCHA_V3_SITE_KEY=${{ secrets.RECAPTCHA_V3_SITE_KEY }}
57+
NEXT_PUBLIC_TURNSTILE_SITE_KEY=${{ secrets.TURNSTILE_SITE_KEY }}
58+
NEXT_PUBLIC_TURNSTILE_SESSION_SITE_KEY=${{ secrets.TURNSTILE_SESSION_SITE_KEY }}
5959
NEXT_PUBLIC_MAPTILER_API_KEY=${{ secrets.NEXT_PUBLIC_MAPTILER_API_KEY }}
6060
NEXT_PUBLIC_S3_BUCKET_URL=https://tilesets1.cdn.districtr.org
6161
NEXT_PUBLIC_S3_BUCKET_URL_MIRROR1=https://tilesets2.cdn.districtr.org

.github/workflows/fly-deploy-api.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ jobs:
2323
echo "app_name=districtr-v2-api" >> $GITHUB_OUTPUT
2424
fi
2525
- uses: superfly/flyctl-actions/setup-flyctl@1.5
26+
# Turnstile cutover: the backend no longer reads RECAPTCHA_*, so stage
27+
# the renamed secrets from GH secrets and drop the old ones; --stage
28+
# means both land atomically with the deploy below. No-op once the
29+
# cutover is done.
30+
- name: Stage Turnstile secrets
31+
env:
32+
FLY_API_TOKEN: ${{ github.ref_name == 'dev' && secrets.FLY_API_DEV_TOKEN || secrets.FLY_API_TOKEN }}
33+
APP: ${{ steps.deploy-env.outputs.app_name }}
34+
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
35+
TURNSTILE_SESSION_SECRET_KEY: ${{ secrets.TURNSTILE_SESSION_SECRET_KEY }}
36+
run: |
37+
set -euo pipefail
38+
args=()
39+
if [ -n "${TURNSTILE_SECRET_KEY:-}" ]; then args+=( TURNSTILE_SECRET_KEY="$TURNSTILE_SECRET_KEY" ); fi
40+
if [ -n "${TURNSTILE_SESSION_SECRET_KEY:-}" ]; then args+=( TURNSTILE_SESSION_SECRET_KEY="$TURNSTILE_SESSION_SECRET_KEY" ); fi
41+
if [ "${#args[@]}" -gt 0 ]; then
42+
flyctl secrets set --stage -a "$APP" "${args[@]}"
43+
else
44+
echo "::warning::TURNSTILE_* GitHub secrets are unset; deploying without captcha secrets"
45+
fi
46+
flyctl secrets unset --stage -a "$APP" RECAPTCHA_SECRET_KEY RECAPTCHA_V3_SECRET_KEY || true
2647
- name: Deploy (dev)
2748
if: github.ref_name == 'dev'
2849
run: flyctl deploy --remote-only --app ${{ steps.deploy-env.outputs.app_name }}

.github/workflows/fly-deploy-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
- name: Write .env.production
2828
run: |
2929
cat <<EOF > .env.production
30-
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=${{ secrets.RECAPTCHA_SITE_KEY }}
30+
NEXT_PUBLIC_TURNSTILE_SITE_KEY=${{ secrets.TURNSTILE_SITE_KEY }}
3131
NEXT_PUBLIC_MAPTILER_API_KEY=${{ secrets.NEXT_PUBLIC_MAPTILER_API_KEY }}
3232
NEXT_PUBLIC_S3_BUCKET_URL=https://tilesets1.cdn.districtr.org
3333
NEXT_PUBLIC_S3_BUCKET_URL_MIRROR1=https://tilesets2.cdn.districtr.org

.github/workflows/fly-preview.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ name: Fly Preview
1515
# Required repo VARS: FLY_ORG, DEV_PG_APP, R2_BUCKET_NAME
1616
# Required repo SECRETS: FLY_PREVIEW_API_TOKEN (org-scoped: `fly tokens create org`),
1717
# AUTH0_SECRET, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_DOMAIN, AUTH0_API_AUDIENCE,
18-
# RECAPTCHA_SITE_KEY, NEXT_PUBLIC_MAPTILER_API_KEY, SENTRY_AUTH_TOKEN (optional);
18+
# TURNSTILE_SITE_KEY, NEXT_PUBLIC_MAPTILER_API_KEY, SENTRY_AUTH_TOKEN (optional);
1919
# fullstack also needs: PREVIEW_DB_USER/PASSWORD/NAME, BE_SECRET_KEY, AUTH0_ISSUER,
20-
# AUTH0_ALGORITHMS, and optionally RECAPTCHA_SECRET_KEY, AWS_ACCESS_KEY_ID/SECRET, R2_ACCOUNT_ID.
20+
# AUTH0_ALGORITHMS, and optionally TURNSTILE_SECRET_KEY, AWS_ACCESS_KEY_ID/SECRET, R2_ACCOUNT_ID.
2121
# One-time: create the two labels, and set BACKEND_CORS_ORIGIN_REGEX on the dev backend
2222
# so FE-only previews are allowed cross-origin (see PR description / config.py).
2323

@@ -114,7 +114,7 @@ jobs:
114114
AUTH0_API_AUDIENCE: ${{ secrets.AUTH0_API_AUDIENCE }}
115115
AUTH0_ISSUER: ${{ secrets.AUTH0_ISSUER }}
116116
AUTH0_ALGORITHMS: RS256
117-
RECAPTCHA_SECRET_KEY: ${{ secrets.RECAPTCHA_SECRET_KEY }}
117+
TURNSTILE_SECRET_KEY: ${{ secrets.TURNSTILE_SECRET_KEY }}
118118
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
119119
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
120120
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
@@ -175,7 +175,7 @@ jobs:
175175
AUTH0_ALGORITHMS="$AUTH0_ALGORITHMS"
176176
)
177177
# Optional secrets — only set when provided.
178-
if [ -n "${RECAPTCHA_SECRET_KEY:-}" ]; then args+=( RECAPTCHA_SECRET_KEY="$RECAPTCHA_SECRET_KEY" ); fi
178+
if [ -n "${TURNSTILE_SECRET_KEY:-}" ]; then args+=( TURNSTILE_SECRET_KEY="$TURNSTILE_SECRET_KEY" ); fi
179179
if [ -n "${AWS_ACCESS_KEY_ID:-}" ]; then args+=( AWS_ACCESS_KEY_ID="$AWS_ACCESS_KEY_ID" ); fi
180180
if [ -n "${AWS_SECRET_ACCESS_KEY:-}" ]; then args+=( AWS_SECRET_ACCESS_KEY="$AWS_SECRET_ACCESS_KEY" ); fi
181181
if [ -n "${R2_ACCOUNT_ID:-}" ]; then args+=( ACCOUNT_ID="$R2_ACCOUNT_ID" ); fi
@@ -212,7 +212,7 @@ jobs:
212212
FE_URL: ${{ steps.setup.outputs.fe_url }}
213213
API_URL: ${{ steps.setup.outputs.api_url }}
214214
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
215-
RECAPTCHA_SITE_KEY: ${{ secrets.RECAPTCHA_SITE_KEY }}
215+
TURNSTILE_SITE_KEY: ${{ secrets.TURNSTILE_SITE_KEY }}
216216
NEXT_PUBLIC_MAPTILER_API_KEY: ${{ secrets.NEXT_PUBLIC_MAPTILER_API_KEY }}
217217
AUTH0_SECRET: ${{ secrets.AUTH0_SECRET }}
218218
AUTH0_CLIENT_ID: ${{ secrets.AUTH0_CLIENT_ID }}
@@ -226,7 +226,7 @@ jobs:
226226
227227
# Build-time public env (baked into the image; Dockerfile COPYs this in).
228228
cat > app/.env.production <<EOF
229-
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY}
229+
NEXT_PUBLIC_TURNSTILE_SITE_KEY=${TURNSTILE_SITE_KEY}
230230
NEXT_PUBLIC_MAPTILER_API_KEY=${NEXT_PUBLIC_MAPTILER_API_KEY}
231231
NEXT_PUBLIC_S3_BUCKET_URL=https://tilesets1.cdn.districtr.org
232232
NEXT_PUBLIC_S3_BUCKET_URL_MIRROR1=https://tilesets2.cdn.districtr.org

app/.env.docker.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ AUTH0_SECRET="fill-me" # Added by `npx auth`. Read more: https://cli.authjs.dev
1717
# Feedback form
1818
NEXT_PUBLIC_FEEDBACK_FORM="fill-me-optionally"
1919

20-
NEXT_PUBLIC_RECAPTCHA_SITE_KEY="fill-me-optionally"
20+
# Cloudflare Turnstile. The values below are Cloudflare's public dummy keys
21+
# (always pass); swap in real widget site keys to test actual challenges, or
22+
# use 3x00000000000000000000FF to force the interactive session fallback.
23+
NEXT_PUBLIC_TURNSTILE_SITE_KEY="1x00000000000000000000AA"
24+
NEXT_PUBLIC_TURNSTILE_SESSION_SITE_KEY="1x00000000000000000000AA"
2125

2226
# MapTiler API key for Streets/Satellite basemaps and geocoding search (optional)
2327
NEXT_PUBLIC_MAPTILER_API_KEY="fill-me-optionally"

app/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ENV NODE_ENV="production"
1616
FROM base as build
1717
ARG NEXT_PUBLIC_API_URL
1818
ARG NEXT_PUBLIC_S3_BUCKET_URL
19-
ARG NEXT_PUBLIC_RECAPTCHA_V3_SITE_KEY
19+
ARG NEXT_PUBLIC_TURNSTILE_SESSION_SITE_KEY
2020

2121
# Install packages needed to build node modules
2222
RUN apt-get update -qq && \
@@ -32,7 +32,7 @@ RUN bun install --include=dev
3232
RUN echo NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL && \
3333
echo "NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL" > .env.production && \
3434
echo "NEXT_PUBLIC_S3_BUCKET_URL=$NEXT_PUBLIC_S3_BUCKET_URL" >> .env.production && \
35-
echo "NEXT_PUBLIC_RECAPTCHA_V3_SITE_KEY=$NEXT_PUBLIC_RECAPTCHA_V3_SITE_KEY" >> .env.production
35+
echo "NEXT_PUBLIC_TURNSTILE_SESSION_SITE_KEY=$NEXT_PUBLIC_TURNSTILE_SESSION_SITE_KEY" >> .env.production
3636

3737
# Copy application code
3838
COPY --link . .

app/bun.lock

Lines changed: 0 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)