diff --git a/public/js/settings.js b/public/js/settings.js index f2a97953..f58e2182 100644 --- a/public/js/settings.js +++ b/public/js/settings.js @@ -1781,11 +1781,12 @@ var widgetPihole = document.getElementById('portal-widget-pihole'); var widgetMidea = document.getElementById('portal-widget-midea'); var widgetSmarthome = document.getElementById('portal-widget-smarthome'); + var widgetSkoda = document.getElementById('portal-widget-skoda'); var trustToggle = document.getElementById('portal-trust-owner-mapping'); var autoappearToggle = document.getElementById('portal-autoappear'); if (!enabledToggle) return; - [enabledToggle, widgetDevice, widgetTraffic, widgetServices, widgetPihole, widgetMidea, widgetSmarthome, trustToggle, autoappearToggle].forEach(function (el) { + [enabledToggle, widgetDevice, widgetTraffic, widgetServices, widgetPihole, widgetMidea, widgetSmarthome, widgetSkoda, trustToggle, autoappearToggle].forEach(function (el) { if (el) el.addEventListener('click', function () { el.classList.toggle('on'); el.dispatchEvent(new Event('change')); @@ -1807,6 +1808,7 @@ setToggle(widgetPihole, d.widgets && d.widgets.pihole); setToggle(widgetMidea, d.widgets && d.widgets.midea); setToggle(widgetSmarthome, d.widgets && d.widgets.smarthome); + setToggle(widgetSkoda, d.widgets && d.widgets.skoda); setToggle(trustToggle, d.trustOwnerMapping); setToggle(autoappearToggle, d.autoappear !== false); if (window.SettingsAutosave && SettingsAutosave.resync) SettingsAutosave.resync('portal'); @@ -1814,7 +1816,7 @@ console.error('Failed to load portal settings:', err); }); - var portalFields = [enabledToggle, widgetDevice, widgetTraffic, widgetServices, widgetPihole, widgetMidea, widgetSmarthome, trustToggle, autoappearToggle].filter(Boolean); + var portalFields = [enabledToggle, widgetDevice, widgetTraffic, widgetServices, widgetPihole, widgetMidea, widgetSmarthome, widgetSkoda, trustToggle, autoappearToggle].filter(Boolean); SettingsAutosave.bind({ cluster: 'portal', fields: portalFields, @@ -1828,6 +1830,7 @@ 'portal-widget-pihole': widgetPihole ? widgetPihole.classList.contains('on') : true, 'portal-widget-midea': widgetMidea ? widgetMidea.classList.contains('on') : true, 'portal-widget-smarthome': widgetSmarthome ? widgetSmarthome.classList.contains('on') : true, + 'portal-widget-skoda': widgetSkoda ? widgetSkoda.classList.contains('on') : true, 'portal-trust-owner-mapping': trustToggle ? trustToggle.classList.contains('on') : false, 'portal-autoappear': autoappearToggle ? autoappearToggle.classList.contains('on') : true, }; @@ -1842,6 +1845,7 @@ pihole: widgetPihole ? widgetPihole.classList.contains('on') : true, midea: widgetMidea ? widgetMidea.classList.contains('on') : true, smarthome: widgetSmarthome ? widgetSmarthome.classList.contains('on') : true, + skoda: widgetSkoda ? widgetSkoda.classList.contains('on') : true, }, trust_owner_mapping: trustToggle ? trustToggle.classList.contains('on') : false, autoappear: autoappearToggle ? autoappearToggle.classList.contains('on') : true, diff --git a/src/i18n/de.json b/src/i18n/de.json index ab935aa8..ba3ae93d 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -1942,6 +1942,7 @@ "settings.portal.widget_pihole": "Pi-hole-Widget", "settings.portal.widget_midea": "Klima-Widget", "settings.portal.widget_smarthome": "Smart Home", + "settings.portal.widget_skoda": "Fahrzeuge", "settings.portal.autoappear": "Portal automatisch öffnen beim Verbinden", "settings.portal.autoappear_help": "Wenn aktiv, öffnen VPN-Clients das Portal nach dem Verbinden automatisch. Deaktivieren, um das Portal erreichbar zu halten, ohne es automatisch zu öffnen.", "settings.portal.trust_owner_mapping": "Gerät→Besitzer-Vertrauen (pro-Besitzer ohne Login)", diff --git a/src/i18n/en.json b/src/i18n/en.json index dc9610d9..9f5baf4b 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1998,6 +1998,7 @@ "settings.portal.widget_pihole": "Pi-hole widget", "settings.portal.widget_midea": "Climate widget", "settings.portal.widget_smarthome": "Smart Home", + "settings.portal.widget_skoda": "Vehicles", "settings.portal.autoappear": "Auto-open portal on connect", "settings.portal.autoappear_help": "When enabled, VPN clients automatically open the portal after connecting. Disable to keep the portal accessible but not auto-opened.", "settings.portal.trust_owner_mapping": "Trust device→owner mapping (zero-login per-owner)", diff --git a/src/routes/api/settings/portal.js b/src/routes/api/settings/portal.js index b909f48f..871a7c87 100644 --- a/src/routes/api/settings/portal.js +++ b/src/routes/api/settings/portal.js @@ -63,6 +63,9 @@ router.put('/portal', (req, res) => { if (widgets.smarthome !== undefined) { settings.set('portal.widget.smarthome', widgets.smarthome ? '1' : '0'); } + if (widgets.skoda !== undefined) { + settings.set('portal.widget.skoda', widgets.skoda ? '1' : '0'); + } if (body.trust_owner_mapping !== undefined) { settings.set('portal.trust_owner_mapping', body.trust_owner_mapping ? '1' : '0'); diff --git a/src/services/skoda/skodaClient.js b/src/services/skoda/skodaClient.js index 50961782..b0576c46 100644 --- a/src/services/skoda/skodaClient.js +++ b/src/services/skoda/skodaClient.js @@ -8,7 +8,10 @@ class SkodaApiError extends Error { } // Hosts observed serving compositeRenders images; confirm/extend via Task-3 spike. -const RENDER_HOST_ALLOWLIST = [/\.azureedge\.net$/, /\.skoda-auto\.cz$/]; +// Hosts observed serving compositeRenders images (live-confirmed). iprenders is +// Skoda's public Azure blob bucket — pinned to the exact host (not all of +// *.blob.core.windows.net, which any Azure tenant can use). +const RENDER_HOST_ALLOWLIST = [/\.azureedge\.net$/, /\.skoda-auto\.cz$/, /^iprenders\.blob\.core\.windows\.net$/]; class SkodaClient { constructor({ getSession, saveSession, fetchImpl = fetch }) { @@ -52,15 +55,14 @@ class SkodaClient { async renderImage(url) { // The url comes from the Skoda API response — never fetch it unvalidated, // and never send our bearer token to an arbitrary host (SSRF/token leak). - // Confirm/extend the allowlist from the Task-3 live spike; if the spike - // shows the CDN serves images unauthenticated, drop the auth header here. let parsed; try { parsed = new URL(url); } catch { throw new SkodaApiError('invalid render url', 'SKODA_API_ERROR', 0); } if (parsed.protocol !== 'https:' || !RENDER_HOST_ALLOWLIST.some((re) => re.test(parsed.hostname))) { throw new SkodaApiError(`render url host not allowed: ${parsed.hostname}`, 'SKODA_API_ERROR', 0); } - const session = this.getSession(); - const res = await this.fetchImpl(parsed.toString(), { headers: { authorization: `Bearer ${session.accessToken}` } }); + // No auth header: the render is a public blob (live-confirmed 200 without + // it), and sending the Skoda access token to a third-party CDN would leak it. + const res = await this.fetchImpl(parsed.toString()); if (res.status >= 400) throw new SkodaApiError(`image fetch failed ${res.status}`, 'SKODA_API_ERROR', res.status); return Buffer.from(await res.arrayBuffer()); } diff --git a/templates/aurora/pages/settings.njk b/templates/aurora/pages/settings.njk index 76d8fbfc..7cf294b3 100644 --- a/templates/aurora/pages/settings.njk +++ b/templates/aurora/pages/settings.njk @@ -992,6 +992,10 @@ {{ t('settings.portal.widget_smarthome') }}
+