From 8906b66bc81a5c9ff2d15beaa42d49de2e67707f Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 24 Sep 2026 16:17:54 +0200 Subject: [PATCH] feat(settings): set the Google Maps key through PLACES_API_KEY The server already put PLACES_API_KEY ahead of the key saved in the admin panel, but nothing said so: the variable was in no template and the admin field kept showing a stored value that no search used. GET /me/settings now answers env_keys, the variable behind each key the environment sets (Google, Unsplash, Amap), and null for that key's value. The admin field turns read-only and names the variable, Test still probes the key a search resolves to, and a save leaves those names out so the stored fallback survives. The variable is documented in .env.example, docker-compose.yml, the Helm chart (as a secret) and the wiki. Addresses discussion #1881. --- charts/trek/templates/deployment.yaml | 6 +++ charts/trek/templates/secret.yaml | 6 +++ charts/trek/values.yaml | 5 ++ .../screens/admin/MAdminSettingsSection.tsx | 10 ++-- client/src/mobile/screens/admin/MAdminUi.tsx | 2 +- .../src/pages/admin/AdminSettingsTab.test.tsx | 17 ++++++- client/src/pages/admin/AdminSettingsTab.tsx | 16 +++---- client/src/pages/admin/useAdmin.test.tsx | 46 ++++++++++++++++++- client/src/pages/admin/useAdmin.ts | 36 +++++++++++---- client/tests/helpers/mobileAdmin.ts | 7 ++- .../admin/MAdminSettingsSection.test.tsx | 17 ++++++- docker-compose.yml | 1 + server/.env.example | 2 + server/src/nest/auth/user-profile.service.ts | 16 +++++-- server/src/nest/settings/instance-api-keys.ts | 21 +++++++++ server/tests/e2e/auth.e2e.test.ts | 16 +++++++ .../unit/app-config/config-templates.test.ts | 6 +-- .../nest/settings/instance-api-keys.test.ts | 41 +++++++++++++++++ .../unit/nest/user-profile.service.test.ts | 24 ++++++++++ shared/src/i18n/ar/admin.ts | 1 + shared/src/i18n/br/admin.ts | 1 + shared/src/i18n/ca/admin.ts | 1 + shared/src/i18n/cs/admin.ts | 1 + shared/src/i18n/de/admin.ts | 1 + shared/src/i18n/en/admin.ts | 1 + shared/src/i18n/es/admin.ts | 1 + shared/src/i18n/fr/admin.ts | 1 + shared/src/i18n/gr/admin.ts | 1 + shared/src/i18n/hu/admin.ts | 1 + shared/src/i18n/id/admin.ts | 1 + shared/src/i18n/it/admin.ts | 1 + shared/src/i18n/ja/admin.ts | 1 + shared/src/i18n/ko/admin.ts | 1 + shared/src/i18n/nl/admin.ts | 1 + shared/src/i18n/pl/admin.ts | 1 + shared/src/i18n/ru/admin.ts | 1 + shared/src/i18n/sv/admin.ts | 1 + shared/src/i18n/tr/admin.ts | 1 + shared/src/i18n/uk/admin.ts | 1 + shared/src/i18n/vi/admin.ts | 1 + shared/src/i18n/zh-TW/admin.ts | 1 + shared/src/i18n/zh/admin.ts | 1 + wiki/Environment-Variables.md | 23 +++++++++- wiki/Install-Helm.md | 1 + wiki/Places-and-Search.md | 2 +- 45 files changed, 308 insertions(+), 36 deletions(-) diff --git a/charts/trek/templates/deployment.yaml b/charts/trek/templates/deployment.yaml index 24e67325c..3f23458a2 100644 --- a/charts/trek/templates/deployment.yaml +++ b/charts/trek/templates/deployment.yaml @@ -75,6 +75,12 @@ spec: name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }} key: UNSPLASH_ACCESS_KEY optional: true + - name: PLACES_API_KEY + valueFrom: + secretKeyRef: + name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }} + key: PLACES_API_KEY + optional: true - name: AMAP_API_KEY valueFrom: secretKeyRef: diff --git a/charts/trek/templates/secret.yaml b/charts/trek/templates/secret.yaml index c465e8e34..1de6e12c2 100644 --- a/charts/trek/templates/secret.yaml +++ b/charts/trek/templates/secret.yaml @@ -20,6 +20,9 @@ data: {{- if .Values.secretEnv.UNSPLASH_ACCESS_KEY }} UNSPLASH_ACCESS_KEY: {{ .Values.secretEnv.UNSPLASH_ACCESS_KEY | b64enc | quote }} {{- end }} + {{- if .Values.secretEnv.PLACES_API_KEY }} + PLACES_API_KEY: {{ .Values.secretEnv.PLACES_API_KEY | b64enc | quote }} + {{- end }} {{- if .Values.secretEnv.AMAP_API_KEY }} AMAP_API_KEY: {{ .Values.secretEnv.AMAP_API_KEY | b64enc | quote }} {{- end }} @@ -56,6 +59,9 @@ stringData: {{- if .Values.secretEnv.UNSPLASH_ACCESS_KEY }} UNSPLASH_ACCESS_KEY: {{ .Values.secretEnv.UNSPLASH_ACCESS_KEY }} {{- end }} + {{- if .Values.secretEnv.PLACES_API_KEY }} + PLACES_API_KEY: {{ .Values.secretEnv.PLACES_API_KEY }} + {{- end }} {{- if .Values.secretEnv.AMAP_API_KEY }} AMAP_API_KEY: {{ .Values.secretEnv.AMAP_API_KEY }} {{- end }} diff --git a/charts/trek/values.yaml b/charts/trek/values.yaml index 0d4bb1aeb..fe35e4c05 100644 --- a/charts/trek/values.yaml +++ b/charts/trek/values.yaml @@ -133,6 +133,11 @@ secretEnv: # Get a free key at https://unsplash.com/developers. Can also be set per-admin # in Admin > Settings; this value overrides that. Leave empty to disable. UNSPLASH_ACCESS_KEY: "" + # Optional Google Maps API key (Places API (New) enabled) for place search, photos, + # details and Google transit. Overrides the key set in Admin > Settings > API Keys, + # whose field then shows this variable read-only. Leave empty to use the key from the + # admin panel, or none. + PLACES_API_KEY: "" # Optional Amap (高德地图) Web 服务 key for place search and geocoding inside mainland # China. Overrides the key set in Admin > Settings > API Keys. A Web 端 (JS API) key is # a different credential and is rejected with INVALID_USER_KEY. Leave empty to use the diff --git a/client/src/mobile/screens/admin/MAdminSettingsSection.tsx b/client/src/mobile/screens/admin/MAdminSettingsSection.tsx index 95e356e4c..6a2fdcd69 100644 --- a/client/src/mobile/screens/admin/MAdminSettingsSection.tsx +++ b/client/src/mobile/screens/admin/MAdminSettingsSection.tsx @@ -44,7 +44,7 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio passkeyLogin, setPasskeyLogin, passkeyConfigured, webauthnRpId, setWebauthnRpId, webauthnOrigins, setWebauthnOrigins, savingWebauthn, handleSaveWebauthn, allowedFileTypes, setAllowedFileTypes, savingFileTypes, setSavingFileTypes, - mapsKey, setMapsKey, unsplashKey, setUnsplashKey, amapKey, setAmapKey, hasMapsKey, hasAmapKey, savingKeys, validating, validation, + mapsKey, setMapsKey, unsplashKey, setUnsplashKey, amapKey, setAmapKey, hasMapsKey, hasAmapKey, keyInputProps, mapsKeyTestable, savingKeys, validating, validation, placesProvider, savingPlacesProvider, handleSavePlacesProvider, managed, setShowRotateJwtModal, @@ -245,12 +245,12 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio aria-label={t('admin.mapsKey')} value={mapsKey} onChange={(e) => setMapsKey(e.target.value)} - placeholder={t('settings.keyPlaceholder')} + {...keyInputProps('maps')} /> handleValidateKey('maps')} className="h-[42px]" @@ -374,7 +374,7 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio aria-label={t('admin.amapKey')} value={amapKey} onChange={(e) => setAmapKey(e.target.value)} - placeholder={t('settings.keyPlaceholder')} + {...keyInputProps('amap')} /> @@ -384,7 +384,7 @@ export default function MAdminSettingsSection({ admin, t }: MAdminSettingsSectio aria-label={t('admin.unsplashKey')} value={unsplashKey} onChange={(e) => setUnsplashKey(e.target.value)} - placeholder={t('settings.keyPlaceholder')} + {...keyInputProps('unsplash')} /> )} diff --git a/client/src/mobile/screens/admin/MAdminUi.tsx b/client/src/mobile/screens/admin/MAdminUi.tsx index 7226adfb5..6d7b187ee 100644 --- a/client/src/mobile/screens/admin/MAdminUi.tsx +++ b/client/src/mobile/screens/admin/MAdminUi.tsx @@ -75,7 +75,7 @@ export function MAdminInput({ className = '', ...props }: InputHTMLAttributes ) } diff --git a/client/src/pages/admin/AdminSettingsTab.test.tsx b/client/src/pages/admin/AdminSettingsTab.test.tsx index d772c1be6..36dba5bcc 100644 --- a/client/src/pages/admin/AdminSettingsTab.test.tsx +++ b/client/src/pages/admin/AdminSettingsTab.test.tsx @@ -1,4 +1,4 @@ -// FE-ADMSET-001 to FE-ADMSET-042 +// FE-ADMSET-001 to FE-ADMSET-043 import { http, HttpResponse } from 'msw'; import React from 'react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -339,6 +339,21 @@ describe('AdminSettingsTab', () => { expect(within(card('API Keys')).getByRole('button', { name: /^test$/i })).toBeDisabled(); }); + it('FE-ADMSET-043: a key the environment sets is read-only and names its variable (#1881)', () => { + const admin = renderTab({ + keyInputProps: (field: string) => + field === 'maps' ? { disabled: true, placeholder: 'Set via PLACES_API_KEY' } : { disabled: false, placeholder: 'Enter key...' }, + mapsKeyTestable: true, + }); + + expect(keyInput('Google Maps API Key')).toBeDisabled(); + expect(keyInput('Google Maps API Key')).toHaveAttribute('placeholder', 'Set via PLACES_API_KEY'); + expect(keyInput(/unsplash/i)).toBeEnabled(); + // The empty field still tests: the probe uses the key a search resolves to. + fireEvent.click(within(card('API Keys')).getByRole('button', { name: /^test$/i })); + expect(admin.handleValidateKey).toHaveBeenCalledWith('maps'); + }); + it('FE-ADMSET-024: the maps Test button validates the maps key', () => { const admin = renderTab({ mapsKey: 'AIza-test' }); diff --git a/client/src/pages/admin/AdminSettingsTab.tsx b/client/src/pages/admin/AdminSettingsTab.tsx index 8526b0fa0..d19251624 100644 --- a/client/src/pages/admin/AdminSettingsTab.tsx +++ b/client/src/pages/admin/AdminSettingsTab.tsx @@ -37,7 +37,7 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R passkeyLogin, setPasskeyLogin, passkeyConfigured, webauthnRpId, setWebauthnRpId, webauthnOrigins, setWebauthnOrigins, savingWebauthn, handleSaveWebauthn, allowedFileTypes, setAllowedFileTypes, savingFileTypes, setSavingFileTypes, - mapsKey, setMapsKey, unsplashKey, setUnsplashKey, amapKey, setAmapKey, hasMapsKey, hasAmapKey, showKeys, savingKeys, validating, validation, + mapsKey, setMapsKey, unsplashKey, setUnsplashKey, amapKey, setAmapKey, hasMapsKey, hasAmapKey, keyInputProps, mapsKeyTestable, showKeys, savingKeys, validating, validation, placesProvider, savingPlacesProvider, handleSavePlacesProvider, managed, setShowRotateJwtModal, @@ -401,8 +401,8 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R type={showKeys.maps ? 'text' : 'password'} value={mapsKey} onChange={e => setMapsKey(e.target.value)} - placeholder={t('settings.keyPlaceholder')} - className="w-full pr-10 px-3 py-2 border border-edge rounded-lg text-sm bg-surface-input text-content focus:ring-2 focus:ring-accent focus:border-transparent" + {...keyInputProps('maps')} + className="w-full pr-10 px-3 py-2 border border-edge rounded-lg text-sm bg-surface-input text-content focus:ring-2 focus:ring-accent focus:border-transparent disabled:opacity-60 disabled:cursor-not-allowed" />