diff --git a/src/cli.ts b/src/cli.ts index 75f497d..54c89a0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -170,6 +170,7 @@ export function registerCliHandlers( 'graphhopper', { profile: params.profile }, settings, + app, ); return [ `Profile: ${result.profileUsed}`, diff --git a/src/geosearch.ts b/src/geosearch.ts index 7222b3e..1d1d23f 100644 --- a/src/geosearch.ts +++ b/src/geosearch.ts @@ -27,10 +27,13 @@ export class GeoSearcher { | geosearch.GoogleProvider = null; private settings: PluginSettings; private urlConvertor: UrlConvertor; + private app: App; constructor(app: App, settings: PluginSettings) { this.settings = settings; this.urlConvertor = new UrlConvertor(app, settings); + this.app = app; + if (settings.searchProvider == 'osm') { if (!settings.osmUser) { new Notice( @@ -45,8 +48,11 @@ export class GeoSearcher { }, }); } else if (settings.searchProvider == 'google') { + //TODO: this can be improved so that it auto-updates when `apiKey` is changed this.searchProvider = new geosearch.GoogleProvider({ - apiKey: settings.geocodingApiKey, + apiKey: app.secretStorage.getSecret( + settings.geocodingApiKeySecret, + ), }); } } @@ -76,13 +82,16 @@ export class GeoSearcher { if ( this.settings.searchProvider == 'google' && this.settings.useGooglePlacesNew2025 && - this.settings.geocodingApiKey + this.settings.geocodingApiKeySecret ) { try { const placesResults = await googlePlacesSearch( query, this.settings, searchArea?.getCenter(), + this.app.secretStorage.getSecret( + this.settings.geocodingApiKeySecret, + ), ); for (const result of placesResults) { results.push({ @@ -129,10 +138,10 @@ export async function googlePlacesSearch( query: string, settings: PluginSettings, centerOfSearch: leaflet.LatLng | null, + googleApiKey: string, ): Promise { if (settings.searchProvider != 'google' || !settings.useGooglePlacesNew2025) return []; - const googleApiKey = settings.geocodingApiKey; // Request body for the new Places API const requestBody = { diff --git a/src/mapContainer.ts b/src/mapContainer.ts index b38d732..a3b65ea 100644 --- a/src/mapContainer.ts +++ b/src/mapContainer.ts @@ -1832,6 +1832,7 @@ export class MapContainer { marker.location, menu, this.settings, + this.app, ); menu.showAtMouseEvent(ev.originalEvent); } diff --git a/src/menus.ts b/src/menus.ts index 96edc3e..8686070 100644 --- a/src/menus.ts +++ b/src/menus.ts @@ -507,6 +507,7 @@ export function populateRouting( geolocation, submenu, settings, + app, ); }); } @@ -538,6 +539,7 @@ export function populateRouting( geolocation, menu, settings, + app, ); menu.showAtMouseEvent(originalEvent); } @@ -553,6 +555,7 @@ export function populateRouteToPoint( geolocation: leaflet.LatLng, menu: Menu, settings: settings.PluginSettings, + app: App, ) { // The first priority is to choose the user-selected routing source. // If there isn't any, we try the real-time (GPS) location. @@ -586,6 +589,7 @@ export function populateRouteToPoint( { profile: cleanedProfile }, mapContainer, settings, + app, ); }); }); diff --git a/src/routing.ts b/src/routing.ts index 1081933..ed03e60 100644 --- a/src/routing.ts +++ b/src/routing.ts @@ -2,7 +2,7 @@ import { MapContainer } from 'src/mapContainer'; import MapViewPlugin from 'src/main'; import { type PluginSettings } from 'src/settings'; import * as leaflet from 'leaflet'; -import { request, Notice } from 'obsidian'; +import { request, Notice, App } from 'obsidian'; import { type GeoJSON } from 'geojson'; type RoutingProvider = 'graphhopper'; @@ -26,8 +26,12 @@ export async function calcRoute( provider: RoutingProvider, params: RoutingParams, settings: PluginSettings, + app: App, ): Promise { - if (!settings.routingGraphHopperApiKey) { + const apiKey = app.secretStorage.getSecret( + settings.routingGraphHopperApiKeySecret, + ); + if (!apiKey) { throw new Error( 'No GraphHopper API key configured in Map View settings.', ); @@ -44,7 +48,7 @@ export async function calcRoute( ...settings.routingGraphHopperExtra, }; const resultContent: any = await request({ - url: `https://graphhopper.com/api/1/route?key=${settings.routingGraphHopperApiKey}`, + url: `https://graphhopper.com/api/1/route?key=${apiKey}`, method: 'POST', body: JSON.stringify(requestBody), headers: { @@ -78,8 +82,9 @@ export async function doRouting( params: RoutingParams, map: MapContainer, settings: PluginSettings, + app: App, ) { - if (!settings.routingGraphHopperApiKey) { + if (!settings.routingGraphHopperApiKeySecret) { new Notice( 'You must first provide a GraphHopper API key in the settings.', ); @@ -92,6 +97,7 @@ export async function doRouting( provider, params, settings, + app, ); map.addFloatingRoute(routingResult); } catch (e) { diff --git a/src/settings.ts b/src/settings.ts index dc99599..e2d3f49 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,5 +1,5 @@ import { LatLng, type PathOptions } from 'leaflet'; -import { type SplitDirection, Notice } from 'obsidian'; +import { type SplitDirection, App, Notice } from 'obsidian'; import { type MapState, type LegacyMapState, mergeStates } from 'src/mapState'; import type MapViewPlugin from 'src/main'; import * as consts from 'src/consts'; @@ -52,7 +52,11 @@ export type PluginSettings = { searchProvider: 'osm' | 'google'; osmUser: string; searchDelayMs: number; + /** + * @deprecated - Use "geocodingApiKeySecret" + */ geocodingApiKey: string; + geocodingApiKeySecret: string; useGooglePlacesNew2025: boolean; googlePlacesDataFields: string; saveHistory: boolean; @@ -72,7 +76,11 @@ export type PluginSettings = { zoomOnGeolinkPreview: number; handleGeolinkContextMenu: boolean; routingUrl: string; + /** + * @deprecated - use "routingGraphHopperApiKeySecret" + */ routingGraphHopperApiKey: string; + routingGraphHopperApiKeySecret: string; routingGraphHopperProfiles: string; routingGraphHopperExtra: any; cacheAllTiles: boolean; @@ -92,6 +100,8 @@ export type DepracatedFields = { defaultTags?: string[]; snippetLines?: number; useGooglePlaces?: boolean; + routingGraphHopperApiKey: string; + geocodingApiKey: string; }; export type MapLightDark = 'auto' | 'light' | 'dark'; @@ -285,6 +295,7 @@ export const DEFAULT_SETTINGS: PluginSettings = { osmUser: '', searchDelayMs: 250, geocodingApiKey: '', + geocodingApiKeySecret: '', useGooglePlacesNew2025: false, googlePlacesDataFields: '', mapSources: [ @@ -317,6 +328,7 @@ export const DEFAULT_SETTINGS: PluginSettings = { routingUrl: 'https://www.google.com/maps/dir/?api=1&origin={x0},{y0}&destination={x1},{y1}', routingGraphHopperApiKey: '', + routingGraphHopperApiKeySecret: '', routingGraphHopperProfiles: 'foot, bike, car', routingGraphHopperExtra: {}, cacheAllTiles: true, @@ -484,6 +496,28 @@ export function convertLegacyGooglePlaces(settings: PluginSettings): boolean { return changed; } +export function convertLegacyAPIKeysToSecretStorage( + settings: PluginSettings & DepracatedFields, + app: App, +): boolean { + let changed = false; + if (settings.geocodingApiKey) { + const key = 'obsidian-map-view-geocoding-apikey'; + app.secretStorage.setSecret(key, settings.geocodingApiKey); + settings.geocodingApiKeySecret = key; + delete settings.geocodingApiKey; + changed = true; + } + if (settings.routingGraphHopperApiKey) { + const key = 'obsidian-map-view-routing-graphhopper-apikey'; + app.secretStorage.setSecret(key, settings.routingGraphHopperApiKey); + settings.geocodingApiKeySecret = key; + delete settings.routingGraphHopperApiKey; + changed = true; + } + return changed; +} + export function convertMarkerIconRulesToDisplayRules( settings: PluginSettings & DepracatedFields, ) { @@ -606,6 +640,13 @@ export async function convertLegacySettings( ); } + if (convertLegacyAPIKeysToSecretStorage(settings, plugin.app)) { + changed = true; + new Notice( + 'Map View: Legacy API keys for Geocoding and/or converted to the new Secret Storage API. You may need to re-link the secret in settings afterwards.', + ); + } + completePartialSavedStates(settings); if (changed) plugin.saveSettings(); diff --git a/src/settingsTab.ts b/src/settingsTab.ts index 767825c..50c405e 100644 --- a/src/settingsTab.ts +++ b/src/settingsTab.ts @@ -5,6 +5,8 @@ import { TextAreaComponent, Setting, DropdownComponent, + SecretComponent, + Component, } from 'obsidian'; import MapViewPlugin from 'src/main'; @@ -142,21 +144,15 @@ export class SettingsTab extends PluginSettingTab { .setDesc( 'If using Google as the geocoding search provider, paste the API key here. See the plugin documentation for more details. Changes are applied after restart.', ) - .addText((component) => { - component - .setValue(this.plugin.settings.geocodingApiKey) - .onChange(async (value) => { - this.plugin.settings.geocodingApiKey = value; - await this.plugin.saveSettings(); - component.inputEl.style.borderColor = value - ? '' - : 'red'; - }); - component.inputEl.style.borderColor = this.plugin.settings - .geocodingApiKey - ? '' - : 'red'; - }); + .addComponent((component) => + new SecretComponent(this.app, component) + .setValue(this.plugin.settings.geocodingApiKeySecret) + .onChange((value) => { + this.plugin.settings.geocodingApiKeySecret = value; + this.plugin.saveSettings(); + component.style.borderColor = value ? '' : 'red'; + }), + ); let googlePlacesControl = new Setting(containerEl) .setName('Use Google Places for searches') .setDesc( @@ -818,19 +814,24 @@ export class SettingsTab extends PluginSettingTab { this.plugin.saveSettings(); }); }); + new Setting(containerEl) .setName('GraphHopper API key') .setDesc( 'You may obtain a free or a paid key from GraphHopper to enable native routing in Map View.', ) - .addText((component) => { - component - .setValue(this.plugin.settings.routingGraphHopperApiKey) - .onChange(async (value: string) => { - this.plugin.settings.routingGraphHopperApiKey = value; + .addComponent((component) => + new SecretComponent(this.app, component) + .setValue( + this.plugin.settings.routingGraphHopperApiKeySecret, + ) + .onChange((value) => { + this.plugin.settings.routingGraphHopperApiKeySecret = + value; this.plugin.saveSettings(); - }); - }); + }), + ); + new Setting(containerEl) .setName('GraphHopper profiles') .setDesc( diff --git a/src/urlConvertor.ts b/src/urlConvertor.ts index 365f49b..7696dce 100644 --- a/src/urlConvertor.ts +++ b/src/urlConvertor.ts @@ -126,6 +126,7 @@ export class UrlConvertor { async getGeolocationFromGoogleLink( url: string, settings: PluginSettings, + app: App, ): Promise { const content = await request({ url: url }); if (this.settings.debug) console.log('Google link: searching url', url); @@ -136,7 +137,9 @@ export class UrlConvertor { const placeName = placeNameMatch[1]; if (this.settings.debug) console.log('Google link: found place name = ', placeName); - const googleApiKey = settings.geocodingApiKey; + const googleApiKey = app.secretStorage.getSecret( + settings.geocodingApiKeySecret, + ); const params = { query: placeName, key: googleApiKey, diff --git a/src/viewControls.ts b/src/viewControls.ts index 72095f7..96a6c08 100644 --- a/src/viewControls.ts +++ b/src/viewControls.ts @@ -604,6 +604,7 @@ export class RoutingControl extends leaflet.Control { marker.location, menu, this.settings, + this.app, ); menu.showAtMouseEvent(ev); }