tablet-checkin/backend/extension-src/options/options.js
jtricerolph 28d3db8039 Extension: request host permission dynamically instead of relying on CORS
Backend CORS is disabled by default (origin: false), and an unpacked
extension's ID isn't fixed/predictable per install anyway, so allowlisting
it server-side would be awkward. options.js now calls
chrome.permissions.request() for the entered base URL on Save — once
granted, the extension's own fetches bypass CORS entirely for that origin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 18:49:28 +00:00

66 lines
2.6 KiB
JavaScript

const baseUrlInput = document.getElementById('baseUrl');
const apiKeyInput = document.getElementById('apiKey');
const statusEl = document.getElementById('status');
async function load() {
const { baseUrl, apiKey } = await chrome.storage.local.get(['baseUrl', 'apiKey']);
if (baseUrl) baseUrlInput.value = baseUrl;
if (apiKey) apiKeyInput.value = apiKey; // deliberately shown back here (options page,
// not the popup) so the value can be corrected without regenerating the key.
}
// tablet-checkin's own base URL isn't known in advance (dev today, a
// different production URL later), so it can't be pre-declared in the
// manifest's host_permissions — Chrome would need republishing/reloading
// every time the URL changed. Requesting it dynamically here instead (needs
// a user gesture, which this Save click provides) means the extension's own
// fetches bypass CORS entirely for that origin once granted, rather than
// depending on the server sending back Access-Control-Allow-Origin for an
// extension origin ID that isn't fixed/predictable per install anyway.
async function save() {
const baseUrl = baseUrlInput.value.trim().replace(/\/$/, '');
const apiKey = apiKeyInput.value.trim();
if (baseUrl) {
const granted = await chrome.permissions.request({ origins: [`${baseUrl}/*`] });
if (!granted) {
statusEl.textContent = 'Not saved — permission for that URL was denied. This extension needs it to talk to tablet-checkin.';
statusEl.style.color = '#dc2626';
return;
}
}
await chrome.storage.local.set({ baseUrl, apiKey });
statusEl.textContent = 'Saved.';
statusEl.style.color = '#16a34a';
}
async function test() {
const { baseUrl, apiKey } = await chrome.storage.local.get(['baseUrl', 'apiKey']);
if (!baseUrl || !apiKey) {
statusEl.textContent = 'Set both fields and save first.';
statusEl.style.color = '#dc2626';
return;
}
statusEl.textContent = 'Testing…';
statusEl.style.color = '#64748b';
try {
const res = await fetch(`${baseUrl}/api/staff/devices`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || `HTTP ${res.status}`);
}
const devices = await res.json();
statusEl.textContent = `Connected — found ${devices.length} tablet(s).`;
statusEl.style.color = '#16a34a';
} catch (err) {
statusEl.textContent = `Failed: ${err.message}`;
statusEl.style.color = '#dc2626';
}
}
document.getElementById('save').addEventListener('click', save);
document.getElementById('test').addEventListener('click', test);
load();