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" />