diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 46733ca1..7b66a215 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -11,6 +11,54 @@ const api = axios.create({ }) // Add auth token and normalize pagination params +// Kiosk device tokens. The kiosk routes are the only ones that write without a +// login, so each is additionally gated on a token an administrator provisioned +// into THAT screen's browser - see the kiosk views, which claim it from a +// ?token= URL once and strip it from the address bar. A token served inside the +// page would be readable by anyone who could load the page, so it is per-device +// and never shipped with the bundle. +// +// One entry per kiosk: the two are separate screens in separate rooms and a +// shared token would mean provisioning one grants the other. +export const KIOSK_TOKEN_KEYS = { + '/printedparts/kiosk/': 'printedparts-kiosk-token', + '/usb/kiosk/': 'usb-kiosk-token', +} + +function kioskTokenFor(url) { + const path = url || '' + for (const [prefix, storageKey] of Object.entries(KIOSK_TOKEN_KEYS)) { + if (path.includes(prefix)) { + try { return localStorage.getItem(storageKey) || '' } catch { return '' } + } + } + return '' +} + +// Claim a kiosk's device token, once, from the URL an admin used to set the +// screen up. Kept in THIS browser and stripped from the address bar straight +// away, so it does not sit there for a shoulder to read or travel in a bookmark +// or a screenshot. +export function claimKioskToken(storageKey) { + try { + const url = new URL(window.location.href) + const token = (url.searchParams.get('token') || '').trim() + if (!token) return + localStorage.setItem(storageKey, token) + url.searchParams.delete('token') + window.history.replaceState({}, '', url.pathname + url.search + url.hash) + } catch { + // A browser refusing localStorage cannot be provisioned. Nothing to + // recover; the routes 404 and the operator sees the standard error. + } +} + +api.interceptors.request.use(config => { + const token = kioskTokenFor(config.url) + if (token) config.headers['X-Kiosk-Token'] = token + return config +}) + api.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { @@ -1501,17 +1549,6 @@ export const measuringtoolsApi = { } // 3D printed parts (printedparts plugin) -// Device token for the parts kiosk, provisioned once per screen and kept in -// that browser. Absent means the routes answer 404, which is what an -// unprovisioned browser should see. -export const KIOSK_TOKEN_KEY = 'printedparts-kiosk-token' - -function kioskHeaders() { - let token = '' - try { token = localStorage.getItem(KIOSK_TOKEN_KEY) || '' } catch { token = '' } - return token ? { 'X-Kiosk-Token': token } : {} -} - export const printedpartsApi = { list(params = {}) { return api.get('/printedparts/items', { params }) @@ -1552,17 +1589,11 @@ export const printedpartsApi = { adjust(printeditemid, data) { return api.post(`/printedparts/items/${printeditemid}/adjust`, data) }, - // The kiosk routes are the only unauthenticated write in the product, so they - // are gated on a per-DEVICE token rather than on the page. It lives in this - // browser's localStorage, put there once by an admin opening the kiosk with - // ?token=... - see PartsKiosk.vue. A token shipped inside the page would be - // readable by anyone who could load it, which is not a gate. kioskItem(itemcode) { - return api.get(`/printedparts/kiosk/item/${encodeURIComponent(itemcode)}`, - { headers: kioskHeaders() }) + return api.get(`/printedparts/kiosk/item/${encodeURIComponent(itemcode)}`) }, kioskTake(data) { - return api.post('/printedparts/kiosk/take', data, { headers: kioskHeaders() }) + return api.post('/printedparts/kiosk/take', data) }, listFiles(printeditemid) { return api.get(`/printedparts/items/${printeditemid}/files`) diff --git a/plugins/computers/frontend/views/CollectorSettings.vue b/plugins/computers/frontend/views/CollectorSettings.vue index c530c76e..46ced119 100644 --- a/plugins/computers/frontend/views/CollectorSettings.vue +++ b/plugins/computers/frontend/views/CollectorSettings.vue @@ -10,6 +10,50 @@ is nothing in it to find and edit.

+
+

Collector behaviour

+
+ + +

+ Comma-separated CIDRs naming this site's routable networks. The server + picks a PC's primary address from these when the agent reports several. +

+
+ +
+ + +

+ Hours without a collector report before a PC counts as silent on the + dashboard. +

+
+ +
+ +
+ +
+ +

+ Master switch for the RMM-style policy pages. Off by default. +

+
+ + +
+
{{ message }}
{{ error }}
@@ -61,13 +105,57 @@