Skip to content

Replace frontend Google Maps key with server-side proxy#849

Open
edwh wants to merge 29 commits into
developfrom
feature/maps-server-proxy
Open

Replace frontend Google Maps key with server-side proxy#849
edwh wants to merge 29 commits into
developfrom
feature/maps-server-proxy

Conversation

@edwh
Copy link
Copy Markdown
Collaborator

@edwh edwh commented May 13, 2026

Summary

  • Eliminates the GOOGLE_API_CONSOLE_KEY from frontend HTML entirely
  • Adds two authenticated Laravel endpoints (/maps/autocomplete, /maps/place-details) that proxy Google Places API calls server-side using the existing IP-restricted backend key
  • Replaces vue-google-autocomplete (which required the JS SDK + frontend key) with a custom PlacesAutocomplete.vue that calls the proxy
  • Removes the gmap.blade.php Google Maps JS SDK include

Why this is safer than HTTP referrer restrictions

HTTP referrer restrictions (as in PR #848) are unreliable — browsers frequently strip or modify the Referer header. This approach eliminates the attack surface entirely: no key appears in any HTML page.

The endpoints sit behind the auth + verifyUserConsent middleware, so only logged-in, consent-verified users can trigger Google API calls. This matches the actual usage — group and event create/edit pages already require authentication.

What changed

File Change
app/Http/Controllers/MapsProxyController.php New — proxies Places Autocomplete and Place Details
routes/web.php New /maps/autocomplete and /maps/place-details routes under auth middleware
resources/js/components/PlacesAutocomplete.vue New — replaces vue-google-autocomplete, calls proxy
resources/js/components/VenueAddress.vue Swap vue-google-autocompletePlacesAutocomplete
resources/js/components/GroupLocation.vue Swap vue-google-autocompletePlacesAutocomplete
resources/views/includes/gmap.blade.php Remove Google Maps JS SDK load
app/Exceptions/Handler.php Bug fix: return 422 (not 500) for ValidationException on JSON requests
storage/api-docs/api-docs.json Add OpenAPI definitions for the two new endpoints
tests/Feature/Maps/MapsProxyTest.php New — 6 tests covering auth, success, and validation

Test plan

  • MapsProxyTest — 6 tests, all passing (auth redirect, autocomplete with faked HTTP, place details with faked HTTP, validation errors)
  • Manual: create/edit a group and confirm address autocomplete works
  • Manual: create/edit an event and confirm venue autocomplete works
  • Verify no GOOGLE_API_CONSOLE_KEY appears in page source on group/event edit pages

🤖 Generated with Claude Code

edwh and others added 29 commits May 12, 2026 13:13
Newer php:8.2-fpm base ships docker.conf instead of zz-docker.conf.
The old code only removed zz-docker.conf, so docker.conf's listen=9000
override survived, making PHP-FPM bind to TCP while nginx expected the
unix socket — causing 502 Bad Gateway on every deploy.

Also, docker.conf carries clear_env=no which is required for Fly.io
secrets to reach PHP-FPM workers. The previous approach of deleting the
file would have stripped that setting.

Fix: patch docker.conf in-place (keep clear_env=no, set unix socket)
instead of deleting it. Also remove zz-docker.conf for old images.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add --default-character-set=utf8mb4 to both mysqldump and mysql import
commands, and set CHARACTER SET utf8mb4 on CREATE DATABASE. Without these
flags, mysqldump silently encodes 4-byte characters (emoji) as latin1,
corrupting them to literal '?' on import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The notifications table has 488k rows with top users having 57k+ entries.
Without created_at in the index the ORDER BY in Laravel's notification
queries forces a filesort over all per-user rows, causing multi-second
queries that overload the single-CPU DB VM.

Index already applied directly to production on 2026-05-12.
Migration guards against re-adding to avoid error on prod.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
User.notifications() override adds WHERE created_at >= now()-1year so all
notification queries (including unreadNotifications()) are bounded. Without
this, users with years of history triggered filesort over 100k+ rows per
page load.

navbar: load with ->take(10)->get() (LIMIT 10 in DB) instead of loading
the full collection and taking in PHP.

API UserController: ->unreadNotifications()->count() instead of loading
the collection into memory and counting in PHP (COUNT(*) vs SELECT *).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r bug

- fly.toml: performance-2x (dedicated CPUs, avoids shared throttling)
- fly-mysql.toml: performance-2x + 4096MB; add --innodb_buffer_pool_size=2800M
  so InnoDB uses ~70% of RAM instead of Docker default 128MB
- startup.sh: view:clear before view:cache so stale compiled views from a
  previous image don't persist across deploys
- navbar.blade.php: multi-line @php/@endphp form — single-line form silently
  fails to compile @php directive on this Blade version, leaving a bare @php
  literal in the output and causing Undefined variable $navbarNotifications

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fly-mysql.toml: add --log_bin_trust_function_creators=ON to mysqld flags
  so it persists across DB restarts (no deploy needed for the live SET GLOBAL
  already applied)
- fly-migrate.sh: SET GLOBAL log_bin_trust_function_creators = 1 via root
  before each import so non-SUPER restarters user can create functions/triggers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gging)

Dockerfile.mysql already has procps; this adds it to the app container so
vmstat/top/ps are available when SSH'd into the restarters machine.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was incorrectly disabled, causing MediawikiApi class-not-found errors in
production logs on every request that touched the wiki code path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rue)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents timeouts on slow DB queries during cold starts or heavy load.
request_terminate_timeout already set to 120s in php-fpm www.conf.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…copy

- Use config('filesystems.default') to determine disk (s3 on Fly, public_uploads in dev)
- Store logo to the correct disk instead of always using public_uploads (ephemeral on Fly)
- Generate _x100 sized copy on the same disk for views that use Network::sizedLogo('_x100')
- Fixes logo uploads disappearing on Fly.io restart and _x100 version not being available

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e counts

- ApiController::homepage_data: replace Party::past()->get() (18k rows into
  PHP) with a single SQL aggregate for participants + hours_volunteered.
  hoursVolunteered() formula expressed as CASE/CEIL(TIMESTAMPDIFF) in SQL.
  Add Cache::lock() to prevent thundering herd on cold cache.
- Device::fixedPoweredCount/fixedUnpoweredCount/poweredCount/unpoweredCount:
  replace withCount()->get() over 215k rows with COUNT(*)+JOIN queries.
  Each was loading the entire devices table into PHP just to sum a count.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ner)

AddUserToDiscourseThreadForEvent uses pcntl_signal() for timeout handling.
Without pcntl the jobs fail immediately and land in failed_jobs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Package addwiki/mediawiki-api was upgraded to v3.1.0 which renamed its
namespace from Mediawiki\Api\ to Addwiki\Mediawiki\Api\. Update all three
files that reference the old namespace and migrate FluentRequest to
ActionRequest, ApiUser to UserAndPassword auth, and the login flow to
use MediaWiki::newFromEndpoint() with constructor-time auth.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was using a custom fxm.php path; xmlrpc.php is the correct standard endpoint.
WP_XMLRPC_PSWD secret also needs trailing dot removed on next deploy:
fly secrets set WP_XMLRPC_PSWD='giannutri15Stone$87'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d CORS for /api/

therestartproject.org embeds /outbound/info/group/{id} in an iframe and calls
/api/group/{id}/stats cross-origin. Fix by:
- nginx: map X-Frame-Options to empty string for /outbound/ routes (omits header)
- Laravel: AddCorsHeaders middleware added to api middleware group

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LogInToWiki: explicit CookieJar + ActionApi constructor injection
  replaces getConfig('cookies') which silently returns bool in Guzzle 7
- fly.toml: WP_XMLRPC_ENDPOINT uses /fxm.php (custom alias on live site)
  not /xmlrpc.php which is blocked by Wordfence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pcntl extension is not available in PHP-FPM on Fly.io, causing all
AddUserToDiscourseThreadForEvent queue jobs to fail with
"Call to undefined function pcntl_signal()".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pcntl_signal/pcntl_alarm were never working correctly: pcntl_signal
threw "undefined function" in some environments, and when available
$this->fail() threw "undefined method" as InteractsWithQueue trait
was not used. $tries = 1 already limits retries without needing a
signal-based timeout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs: (1) InteractsWithQueue was imported as the Contracts interface
not used as a trait, so $this->fail() was undefined. (2) pcntl_signal
is not available in all environments. Fix: use the Queue trait for
fail() support, guard pcntl calls with function_exists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
$event->theGroup->archived_at was accessed before the $event null guard,
causing failures when events or groups had been deleted (timing windows).
Move the archived_at check inside the $event && $user guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The block form @php...@endphp is not compiled correctly by the Blade
version on this container: @php is emitted as literal text while @endphp
compiles to ?>, leaving $navbarNotifications undefined at runtime (500).
The parenthesised @php($expr) form compiles correctly everywhere.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
castBooleansToString(string|bool) rejects null in PHP 8, thrown when
$user->biography is null (nullable column). Simplest fix: set 'bio'
to null in discourse config so the field is skipped entirely.

Also updates live-patch-status.md with navbar @php fix notes and
DISCOURSE_SECRET putenv workaround (system env had D$ truncated at #).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
castBooleansToString(string|bool) rejects null in PHP 8, thrown when
$user->biography is null (nullable column). Adding a getBiographyAttribute
accessor ensures the field always resolves to a string, so bio syncing
works correctly without skipping the field entirely.

Reverts the 'bio' => null workaround from the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document bootstrap/app.php monkey-patch (sets $_ENV/$_SERVER/putenv for all
broken fly secrets — PHP-FPM clears env so only CLI/queue was affected) and
AddUserToDiscourseThreadForEvent.php pcntl fix (146 failed jobs retried).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A queue job making a blocking HTTP call (no timeout, no pcntl_alarm available
on this container) can hang indefinitely and block the entire queue. The
watchdog runs every minute, checks for jobs reserved >120s, and kills the
worker — supervisord restarts it automatically.

Permanent fix lands on next deploy (pcntl is in Dockerfile.fly). startup.sh
re-registers the cron entry on restart so it works before then too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eliminates the GOOGLE_API_CONSOLE_KEY from frontend HTML entirely.
Places Autocomplete and Place Details calls are now proxied through
authenticated Laravel endpoints (/maps/autocomplete, /maps/place-details)
using the existing IP-restricted backend key.

- Add MapsProxyController with autocomplete and placeDetails methods
- Add /maps/* routes inside the auth+verifyUserConsent middleware group
- Replace vue-google-autocomplete with PlacesAutocomplete.vue (custom
  component that calls the proxy instead of the Google JS SDK)
- Update VenueAddress.vue and GroupLocation.vue to use PlacesAutocomplete
- Remove Google Maps JS SDK load from gmap.blade.php
- Fix Handler.php to return 422 (not 500) for ValidationException in JSON requests
- Add OpenAPI definitions for the two new endpoints
- Add feature tests (MapsProxyTest) covering auth, success, and validation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Comment on lines +14 to +15
return response('', 200)
->header('Access-Control-Allow-Origin', '*')
}

$response = $next($request);
$response->headers->set('Access-Control-Allow-Origin', '*');
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants