diff --git a/extensions/chrome/README.md b/extensions/chrome/README.md index c36d25d274..3beed3de29 100644 --- a/extensions/chrome/README.md +++ b/extensions/chrome/README.md @@ -19,12 +19,64 @@ Onyx connects with dozens of popular workplace apps like Google Drive, Jira, Con For Onyx Cloud Users, please visit the Chrome Plugin Store (pending approval still) +## Enterprise configuration (managed policy) + +Admins can pre-set and lock settings with Chrome's extension policy +(`ExtensionSettings` / "Managed storage" in Google Admin, or the +`3rdparty.extensions.` policy on Windows/macOS/Linux). Settings set by +policy are read-only in the extension UI. Supported keys (see +`managed_schema.json`): + +| Key | Type | Description | +| ---------------------------- | ------- | ----------------------------------------------- | +| `onyxExtensionDomain` | string | Root URL of your Onyx instance | +| `onyxExtensionDefaultNewTab` | boolean | Force the "Use Onyx as new tab page" toggle | + +Example (Linux, `/etc/opt/chrome/policies/managed/onyx.json`; the Chrome Web +Store extension ID is `dacfbnglakogghooelgjflkhjcdbemia`): + +```json +{ + "3rdparty": { + "extensions": { + "dacfbnglakogghooelgjflkhjcdbemia": { + "onyxExtensionDomain": "https://onyx.example.com", + "onyxExtensionDefaultNewTab": false + } + } + } +} +``` + +Trailing slashes on `onyxExtensionDomain` are stripped automatically. + ## Development - Load unpacked extension in your browser - Modify files in `src` directory - Refresh extension in Chrome +### Testing managed policy locally + +Use the ID shown on the unpacked extension's card in `chrome://extensions` +(it differs from the Web Store ID). + +Linux: write the JSON above to `/etc/opt/chrome/policies/managed/onyx.json`. + +macOS: Chrome only reads extension policy from managed preferences, so +`defaults write` is not enough. Write the plist with `plutil`: + +```sh +P="/Library/Managed Preferences/$USER/com.google.Chrome.extensions..plist" +sudo plutil -create xml1 "$P" +sudo plutil -insert onyxExtensionDomain -string "https://onyx.example.com" "$P" +sudo plutil -insert onyxExtensionDefaultNewTab -bool false "$P" +sudo killall cfprefsd +``` + +Then quit and relaunch Chrome, click "Reload policies" on `chrome://policy`, +and reload the extension. Remove the file and restart Chrome to undo. + ## Contributing Submit issues or pull requests for improvements diff --git a/extensions/chrome/managed_schema.json b/extensions/chrome/managed_schema.json new file mode 100644 index 0000000000..158dd0bfbf --- /dev/null +++ b/extensions/chrome/managed_schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-03/schema#", + "type": "object", + "properties": { + "onyxExtensionDomain": { + "title": "Onyx root domain", + "description": "Root URL of the Onyx instance (for example https://cloud.onyx.app). When set by policy, users cannot change it in the extension.", + "type": "string" + }, + "onyxExtensionDefaultNewTab": { + "title": "Use Onyx as new tab page", + "description": "Force the new tab override on (true) or off (false). When set by policy, users cannot change the toggle in the extension.", + "type": "boolean" + } + } +} diff --git a/extensions/chrome/manifest.json b/extensions/chrome/manifest.json index 107c2c8991..64cbfafcb1 100644 --- a/extensions/chrome/manifest.json +++ b/extensions/chrome/manifest.json @@ -1,14 +1,9 @@ { "manifest_version": 3, "name": "Onyx", - "version": "1.1", + "version": "1.2", "description": "Onyx lets you research, create, and automate with LLMs powered by your team's unique knowledge", - "permissions": [ - "sidePanel", - "storage", - "activeTab", - "tabs" - ], + "permissions": ["sidePanel", "storage", "activeTab", "tabs"], "host_permissions": [""], "background": { "service_worker": "service_worker.js", @@ -28,6 +23,9 @@ "128": "public/icon128.png" }, "options_page": "src/pages/options.html", + "storage": { + "managed_schema": "managed_schema.json" + }, "chrome_url_overrides": { "newtab": "src/pages/onyx_home.html" }, diff --git a/extensions/chrome/service_worker.js b/extensions/chrome/service_worker.js index 70ccb7e6c3..bec0d124ae 100644 --- a/extensions/chrome/service_worker.js +++ b/extensions/chrome/service_worker.js @@ -1,9 +1,14 @@ import { - DEFAULT_ONYX_DOMAIN, CHROME_SPECIFIC_STORAGE_KEYS, ACTIONS, SIDE_PANEL_PATH, } from "./src/utils/constants.js"; +import { + getOnyxDomain, + getUseOnyxAsDefaultNewTab, + setUseOnyxAsDefaultNewTab, + isNewTabOverrideChange, +} from "./src/utils/storage.js"; // Track side panel state per window const sidePanelOpenState = new Map(); @@ -52,12 +57,8 @@ async function sendToOnyx(info, tab) { const currentUrl = encodeURIComponent(tab.url); try { - const result = await chrome.storage.local.get({ - [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN, - }); - const url = `${ - result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] - }${SIDE_PANEL_PATH}?user-prompt=${selectedText}`; + const domain = await getOnyxDomain(); + const url = `${domain}${SIDE_PANEL_PATH}?user-prompt=${selectedText}`; await openSidePanel(tab.id); chrome.runtime.sendMessage({ @@ -72,14 +73,12 @@ async function sendToOnyx(info, tab) { async function toggleNewTabOverride() { try { - const result = await chrome.storage.local.get( - CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB - ); - const newValue = - !result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; - await chrome.storage.local.set({ - [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: newValue, - }); + const newValue = !(await getUseOnyxAsDefaultNewTab()); + const updated = await setUseOnyxAsDefaultNewTab(newValue); + if (!updated) { + console.warn("New tab override is managed by policy; ignoring toggle"); + return; + } chrome.notifications.create({ type: "basic", @@ -176,28 +175,16 @@ async function sendActiveTabUrlToPanel() { chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === ACTIONS.GET_CURRENT_ONYX_DOMAIN) { - chrome.storage.local.get( - { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN }, - (result) => { - sendResponse({ - [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: - result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN], - }); - } - ); + getOnyxDomain().then((domain) => { + sendResponse({ [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: domain }); + }); return true; } if (request.action === ACTIONS.CLOSE_SIDE_PANEL) { closeSidePanel(); - chrome.storage.local.get( - { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN }, - (result) => { - chrome.tabs.create({ - url: `${result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]}/auth/login`, - active: true, - }); - } - ); + getOnyxDomain().then((domain) => { + chrome.tabs.create({ url: `${domain}/auth/login`, active: true }); + }); return true; } if (request.action === ACTIONS.OPEN_SIDE_PANEL_WITH_INPUT) { @@ -206,38 +193,34 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { const windowId = sender.tab?.windowId; if (tabId && windowId) { - chrome.storage.local.get( - { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN }, - (result) => { - const encodedText = encodeUserPrompt(selectedText); - const onyxDomain = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; - const url = `${onyxDomain}${SIDE_PANEL_PATH}?user-prompt=${encodedText}`; + getOnyxDomain().then((onyxDomain) => { + const encodedText = encodeUserPrompt(selectedText); + const url = `${onyxDomain}${SIDE_PANEL_PATH}?user-prompt=${encodedText}`; - chrome.storage.session.set({ - pendingInput: { + chrome.storage.session.set({ + pendingInput: { + url: url, + pageUrl: pageUrl, + timestamp: Date.now(), + }, + }); + + chrome.sidePanel + .open({ windowId }) + .then(() => { + chrome.runtime.sendMessage({ + action: ACTIONS.OPEN_ONYX_WITH_INPUT, url: url, pageUrl: pageUrl, - timestamp: Date.now(), - }, - }); - - chrome.sidePanel - .open({ windowId }) - .then(() => { - chrome.runtime.sendMessage({ - action: ACTIONS.OPEN_ONYX_WITH_INPUT, - url: url, - pageUrl: pageUrl, - }); - }) - .catch((error) => { - console.error( - "[Onyx SW] Error opening side panel with text:", - error - ); }); - } - ); + }) + .catch((error) => { + console.error( + "[Onyx SW] Error opening side panel with text:", + error + ); + }); + }); } else { console.error("[Onyx SW] Missing tabId or windowId"); } @@ -255,15 +238,12 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }); chrome.storage.onChanged.addListener((changes, namespace) => { - if ( - namespace === "local" && - changes[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB] - ) { + if (isNewTabOverrideChange(changes, namespace)) { const newValue = changes[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB] .newValue; - if (newValue === false) { + if (namespace === "local" && newValue === false) { chrome.runtime.openOptionsPage(); } } @@ -279,11 +259,7 @@ chrome.omnibox.setDefaultSuggestion({ chrome.omnibox.onInputEntered.addListener(async (text) => { try { - const result = await chrome.storage.local.get({ - [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN, - }); - - const domain = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; + const domain = await getOnyxDomain(); const searchUrl = `${domain}/chat?user-prompt=${encodeURIComponent(text)}`; chrome.tabs.update({ url: searchUrl }); diff --git a/extensions/chrome/src/pages/onyx_home.js b/extensions/chrome/src/pages/onyx_home.js index ecb6cd9c17..372203b5c3 100644 --- a/extensions/chrome/src/pages/onyx_home.js +++ b/extensions/chrome/src/pages/onyx_home.js @@ -8,7 +8,12 @@ import { hideErrorModal, initErrorModal, } from "../utils/error-modal.js"; -import { getOnyxDomain } from "../utils/storage.js"; +import { + getOnyxDomain, + getUseOnyxAsDefaultNewTab, + setUseOnyxAsDefaultNewTab, + isNewTabOverrideChange, +} from "../utils/storage.js"; (function () { let mainIframe = document.getElementById("onyx-iframe"); @@ -109,38 +114,26 @@ import { getOnyxDomain } from "../utils/storage.js"; }); } - function checkOnyxPreference() { - chrome.storage.local.get( - [ - CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB, - CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN, - ], - (items) => { - let useOnyxAsDefaultNewTab = - items[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + async function checkOnyxPreference() { + let useOnyxAsDefaultNewTab = await getUseOnyxAsDefaultNewTab(); - if (useOnyxAsDefaultNewTab === undefined) { - useOnyxAsDefaultNewTab = !!( - localStorage.getItem( - CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB - ) === "1" - ); - chrome.storage.local.set({ - [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: - useOnyxAsDefaultNewTab, - }); - } + if (useOnyxAsDefaultNewTab === undefined) { + useOnyxAsDefaultNewTab = + localStorage.getItem( + CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB + ) === "1"; + await setUseOnyxAsDefaultNewTab(useOnyxAsDefaultNewTab); + } - if (!useOnyxAsDefaultNewTab) { - chrome.tabs.update({ - url: "chrome://new-tab-page", - }); - return; - } + if (!useOnyxAsDefaultNewTab) { + chrome.tabs.update({ + url: "chrome://new-tab-page", + }); + return; + } - setIframeSrc(items[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] + "/nrf"); - } - ); + const domain = await getOnyxDomain(); + setIframeSrc(domain + "/nrf"); } function loadThemeAndBackground() { @@ -209,19 +202,29 @@ import { getOnyxDomain } from "../utils/storage.js"; } chrome.storage.onChanged.addListener(function (changes, namespace) { - if (namespace === "local" && changes.useOnyxAsDefaultNewTab) { + if (isNewTabOverrideChange(changes, namespace)) { checkOnyxPreference(); } }); - window.addEventListener("message", function (event) { + // Only accept messages from one of our own Onyx iframes, and only when + // the sender origin matches that iframe's src (fails closed). + function isTrustedIframeMessage(event) { + const frame = [mainIframe, preloadedIframe].find( + (f) => f && f.contentWindow === event.source + ); + if (!frame) return false; try { - if (event.origin !== getOnyxOrigin(mainIframe)) return; + return getOnyxOrigin(frame) === event.origin; } catch { - return; + return false; } + } + + window.addEventListener("message", function (event) { + if (!isTrustedIframeMessage(event)) return; if (event.data.type === CHROME_MESSAGE.SET_DEFAULT_NEW_TAB) { - chrome.storage.local.set({ useOnyxAsDefaultNewTab: event.data.value }); + setUseOnyxAsDefaultNewTab(event.data.value); } else if (event.data.type === CHROME_MESSAGE.ONYX_APP_LOADED) { clearTimeout(iframeLoadTimeout); hideErrorModal(); diff --git a/extensions/chrome/src/pages/options.html b/extensions/chrome/src/pages/options.html index 754aff17e6..1b70b372f9 100644 --- a/extensions/chrome/src/pages/options.html +++ b/extensions/chrome/src/pages/options.html @@ -259,6 +259,19 @@ color: var(--text-light-03); } + .managed-notice { + font-size: 12px; + color: var(--text-light-03); + padding: 0 12px 12px; + display: none; + } + + .input-field:disabled, + .toggle-switch input:disabled + .toggle-slider { + opacity: 0.5; + cursor: not-allowed; + } + .setting-divider { height: 1px; background: var(--white-10); @@ -460,6 +473,9 @@ placeholder="https://cloud.onyx.app" /> +
+ This setting is managed by your organization. +
@@ -472,6 +488,9 @@
+
+ This setting is managed by your organization. +
diff --git a/extensions/chrome/src/pages/options.js b/extensions/chrome/src/pages/options.js index a62d211dfc..1475b8de5e 100644 --- a/extensions/chrome/src/pages/options.js +++ b/extensions/chrome/src/pages/options.js @@ -1,11 +1,16 @@ +import { CHROME_SPECIFIC_STORAGE_KEYS } from "../utils/constants.js"; import { - CHROME_SPECIFIC_STORAGE_KEYS, - DEFAULT_ONYX_DOMAIN, -} from "../utils/constants.js"; + getOnyxDomain, + getUseOnyxAsDefaultNewTab, + getManagedSettings, + normalizeOnyxDomain, +} from "../utils/storage.js"; document.addEventListener("DOMContentLoaded", function () { const domainInput = document.getElementById("onyxDomain"); const useOnyxAsDefaultToggle = document.getElementById("useOnyxAsDefault"); + const domainManagedNotice = document.getElementById("domainManagedNotice"); + const newTabManagedNotice = document.getElementById("newTabManagedNotice"); const statusContainer = document.getElementById("statusContainer"); const statusElement = document.getElementById("status"); const newTabButton = document.getElementById("newTab"); @@ -29,49 +34,70 @@ document.addEventListener("DOMContentLoaded", function () { } } - function loadStoredValues() { - chrome.storage.local.get( - { - [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN, - [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: false, + let domainManaged = false; + let newTabManaged = false; + + async function loadStoredValues() { + const managed = await getManagedSettings(); + domainManaged = + typeof managed[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] === "string"; + newTabManaged = + typeof managed[ + CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB + ] === "boolean"; + + const [domain, useOnyxAsDefault, themeResult] = await Promise.all([ + getOnyxDomain(), + getUseOnyxAsDefaultNewTab(), + chrome.storage.local.get({ [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: "dark", - }, - (result) => { - if (domainInput) - domainInput.value = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; - if (useOnyxAsDefaultToggle) - useOnyxAsDefaultToggle.checked = - result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + }), + ]); - currentTheme = result[CHROME_SPECIFIC_STORAGE_KEYS.THEME] || "dark"; - updateThemeIcon(currentTheme); + if (domainInput) { + domainInput.value = domain; + domainInput.disabled = domainManaged; + } + if (domainManagedNotice) { + domainManagedNotice.style.display = domainManaged ? "block" : "none"; + } + if (useOnyxAsDefaultToggle) { + useOnyxAsDefaultToggle.checked = !!useOnyxAsDefault; + useOnyxAsDefaultToggle.disabled = newTabManaged; + } + if (newTabManagedNotice) { + newTabManagedNotice.style.display = newTabManaged ? "block" : "none"; + } - document.body.className = currentTheme === "light" ? "light-theme" : ""; - } - ); + currentTheme = themeResult[CHROME_SPECIFIC_STORAGE_KEYS.THEME] || "dark"; + updateThemeIcon(currentTheme); + + document.body.className = currentTheme === "light" ? "light-theme" : ""; } function saveSettings() { - const domain = domainInput.value.trim(); const useOnyxAsDefault = useOnyxAsDefaultToggle ? useOnyxAsDefaultToggle.checked : false; - chrome.storage.local.set( - { - [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: domain, - [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: - useOnyxAsDefault, - [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme, - }, - () => { - showStatusMessage( - useOnyxAsDefault - ? "Settings updated. Open a new tab to test it out. Click on the extension icon to bring up Onyx from any page." - : "Settings updated." - ); - } - ); + const values = { [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme }; + if (!domainManaged) { + values[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] = normalizeOnyxDomain( + domainInput.value + ); + } + if (!newTabManaged) { + values[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB] = + useOnyxAsDefault; + } + + chrome.storage.local.set(values, () => { + showStatusMessage( + useOnyxAsDefault + ? "Settings updated. Open a new tab to test it out. Click on the extension icon to bring up Onyx from any page." + : "Settings updated." + ); + }); } function showStatusMessage(message) { @@ -124,8 +150,17 @@ document.addEventListener("DOMContentLoaded", function () { clearTimeout(domainInput.saveTimeout); domainInput.saveTimeout = setTimeout(saveSettings, 1000); }); + domainInput.addEventListener("blur", () => { + domainInput.value = normalizeOnyxDomain(domainInput.value); + }); } + chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === "managed") { + loadStoredValues(); + } + }); + if (useOnyxAsDefaultToggle) { useOnyxAsDefaultToggle.addEventListener("change", saveSettings); } diff --git a/extensions/chrome/src/pages/popup.html b/extensions/chrome/src/pages/popup.html index 78fe01ed12..22c310da84 100644 --- a/extensions/chrome/src/pages/popup.html +++ b/extensions/chrome/src/pages/popup.html @@ -197,6 +197,18 @@ input:checked + .toggle-slider:before { transform: translateX(20px); } + + .toggle-switch input:disabled + .toggle-slider { + opacity: 0.5; + cursor: not-allowed; + } + + .managed-notice { + font-size: 12px; + color: var(--text-light-03); + padding: 0 12px 12px; + display: none; + } @@ -218,6 +230,9 @@ +
+ Managed by your organization. +
diff --git a/extensions/chrome/src/pages/popup.js b/extensions/chrome/src/pages/popup.js index e31f2ec9c1..1d097cef3e 100644 --- a/extensions/chrome/src/pages/popup.js +++ b/extensions/chrome/src/pages/popup.js @@ -1,25 +1,34 @@ -import { CHROME_SPECIFIC_STORAGE_KEYS } from "../utils/constants.js"; +import { + getUseOnyxAsDefaultNewTab, + setUseOnyxAsDefaultNewTab, + isNewTabOverrideManaged, +} from "../utils/storage.js"; document.addEventListener("DOMContentLoaded", async function () { const defaultNewTabToggle = document.getElementById("defaultNewTabToggle"); const openSidePanelButton = document.getElementById("openSidePanel"); const openOptionsButton = document.getElementById("openOptions"); + const newTabManagedNotice = document.getElementById("newTabManagedNotice"); async function loadSetting() { - const result = await chrome.storage.local.get({ - [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: false, - }); + const [value, managed] = await Promise.all([ + getUseOnyxAsDefaultNewTab(), + isNewTabOverrideManaged(), + ]); if (defaultNewTabToggle) { - defaultNewTabToggle.checked = - result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + defaultNewTabToggle.checked = !!value; + defaultNewTabToggle.disabled = managed; + } + if (newTabManagedNotice) { + newTabManagedNotice.style.display = managed ? "block" : "none"; } } async function toggleSetting() { - const currentValue = defaultNewTabToggle.checked; - await chrome.storage.local.set({ - [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: currentValue, - }); + const updated = await setUseOnyxAsDefaultNewTab( + defaultNewTabToggle.checked + ); + if (!updated) await loadSetting(); } async function openSidePanel() { diff --git a/extensions/chrome/src/pages/welcome.html b/extensions/chrome/src/pages/welcome.html index 961f6bb508..866ddb0d2c 100644 --- a/extensions/chrome/src/pages/welcome.html +++ b/extensions/chrome/src/pages/welcome.html @@ -376,6 +376,19 @@ line-height: 1.4; } + .managed-notice { + font-size: 13px; + color: var(--text-light-03); + margin-top: 8px; + display: none; + } + + .input-field:disabled, + .toggle-switch input:disabled + .toggle-slider { + opacity: 0.5; + cursor: not-allowed; + } + .setting-divider { height: 1px; background: var(--white-10); @@ -573,6 +586,9 @@ class="input-field" placeholder="https://cloud.onyx.app" /> +
+ This setting is managed by your organization. +
@@ -602,6 +618,9 @@
+
+ This setting is managed by your organization. +
diff --git a/extensions/chrome/src/pages/welcome.js b/extensions/chrome/src/pages/welcome.js index 6b5baab56d..bc9dcd1476 100644 --- a/extensions/chrome/src/pages/welcome.js +++ b/extensions/chrome/src/pages/welcome.js @@ -1,11 +1,11 @@ -import { - CHROME_SPECIFIC_STORAGE_KEYS, - DEFAULT_ONYX_DOMAIN, -} from "../utils/constants.js"; +import { CHROME_SPECIFIC_STORAGE_KEYS } from "../utils/constants.js"; +import { getManagedSettings, normalizeOnyxDomain } from "../utils/storage.js"; document.addEventListener("DOMContentLoaded", function () { const domainInput = document.getElementById("onyxDomain"); const useOnyxAsDefaultToggle = document.getElementById("useOnyxAsDefault"); + const domainManagedNotice = document.getElementById("domainManagedNotice"); + const newTabManagedNotice = document.getElementById("newTabManagedNotice"); const continueBtn = document.getElementById("continueBtn"); const backBtn = document.getElementById("backBtn"); const finishBtn = document.getElementById("finishBtn"); @@ -18,6 +18,8 @@ document.addEventListener("DOMContentLoaded", function () { let currentStep = 1; let currentTheme = "dark"; + let domainManaged = false; + let newTabManaged = false; // Initialize theme based on system preference or stored value function initTheme() { @@ -121,47 +123,79 @@ document.addEventListener("DOMContentLoaded", function () { } function handleFinish() { - const domain = domainInput.value.trim() || DEFAULT_ONYX_DOMAIN; const useOnyxAsDefault = useOnyxAsDefaultToggle.checked; - chrome.storage.local.set( - { - [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: domain, - [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: - useOnyxAsDefault, - [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme, - [CHROME_SPECIFIC_STORAGE_KEYS.ONBOARDING_COMPLETE]: true, - }, - () => { - // Open a new tab if they enabled the new tab feature, otherwise just close - if (useOnyxAsDefault) { - chrome.tabs.create({}, () => { - window.close(); - }); - } else { + const values = { + [CHROME_SPECIFIC_STORAGE_KEYS.THEME]: currentTheme, + [CHROME_SPECIFIC_STORAGE_KEYS.ONBOARDING_COMPLETE]: true, + }; + if (!domainManaged) { + values[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] = normalizeOnyxDomain( + domainInput.value + ); + } + if (!newTabManaged) { + values[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB] = + useOnyxAsDefault; + } + + chrome.storage.local.set(values, () => { + // Open a new tab if they enabled the new tab feature, otherwise just close + if (useOnyxAsDefault) { + chrome.tabs.create({}, () => { window.close(); - } + }); + } else { + window.close(); } - ); + }); + } + + // Lock controls that are set by policy. Unmanaged controls keep whatever + // the user has entered so far. + async function applyManagedSettings() { + const managed = await getManagedSettings(); + const managedDomain = managed[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; + const managedNewTab = + managed[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + domainManaged = typeof managedDomain === "string"; + newTabManaged = typeof managedNewTab === "boolean"; + + domainInput.disabled = domainManaged; + domainManagedNotice.style.display = domainManaged ? "block" : "none"; + if (domainManaged) { + domainInput.value = normalizeOnyxDomain(managedDomain); + } + + useOnyxAsDefaultToggle.disabled = newTabManaged; + newTabManagedNotice.style.display = newTabManaged ? "block" : "none"; + if (newTabManaged) { + useOnyxAsDefaultToggle.checked = managedNewTab; + } } // Load any existing values (in case user returns to this page) - function loadStoredValues() { - chrome.storage.local.get( - { - [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: "", - [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: true, - }, - (result) => { - if (result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]) { - domainInput.value = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; - } - useOnyxAsDefaultToggle.checked = - result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; - } - ); + async function loadStoredValues() { + const result = await chrome.storage.local.get({ + [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: "", + [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: true, + }); + + if (result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]) { + domainInput.value = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; + } + useOnyxAsDefaultToggle.checked = + result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + + await applyManagedSettings(); } + chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === "managed") { + applyManagedSettings(); + } + }); + if (themeToggle) { themeToggle.addEventListener("click", toggleTheme); } @@ -185,6 +219,11 @@ document.addEventListener("DOMContentLoaded", function () { handleContinue(); } }); + domainInput.addEventListener("blur", () => { + if (domainInput.value.trim()) { + domainInput.value = normalizeOnyxDomain(domainInput.value); + } + }); } initTheme(); diff --git a/extensions/chrome/src/utils/error-modal.js b/extensions/chrome/src/utils/error-modal.js index 8b7b75e431..292bf06418 100644 --- a/extensions/chrome/src/utils/error-modal.js +++ b/extensions/chrome/src/utils/error-modal.js @@ -1,8 +1,9 @@ +import { ACTIONS } from "./constants.js"; import { - CHROME_SPECIFIC_STORAGE_KEYS, - DEFAULT_ONYX_DOMAIN, - ACTIONS, -} from "./constants.js"; + getOnyxDomain, + isNewTabOverrideManaged, + setUseOnyxAsDefaultNewTab, +} from "./storage.js"; const errorModalHTML = `
@@ -277,8 +278,16 @@ export function initErrorModal() { chrome.runtime.openOptionsPage(); }); + isNewTabOverrideManaged().then((managed) => { + if (managed) disableOverrideButton.style.display = "none"; + }); + disableOverrideButton.addEventListener("click", () => { - chrome.storage.local.set({ useOnyxAsDefaultNewTab: false }, () => { + setUseOnyxAsDefaultNewTab(false).then((updated) => { + if (!updated) { + console.warn("New tab override is managed by policy; not disabled."); + return; + } chrome.tabs.update({ url: "chrome://new-tab-page" }); }); }); @@ -325,37 +334,30 @@ export function initAuthModal() { openAuthButton.addEventListener("click", (e) => { e.preventDefault(); - chrome.storage.local.get( - { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN }, - (result) => { - const onyxDomain = result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; - chrome.runtime.sendMessage( - { action: ACTIONS.CLOSE_SIDE_PANEL }, - () => { + getOnyxDomain().then((onyxDomain) => { + chrome.runtime.sendMessage({ action: ACTIONS.CLOSE_SIDE_PANEL }, () => { + if (chrome.runtime.lastError) { + console.error( + "Error closing side panel:", + chrome.runtime.lastError + ); + } + chrome.tabs.create( + { + url: `${onyxDomain}/auth/login`, + active: true, + }, + (_) => { if (chrome.runtime.lastError) { console.error( - "Error closing side panel:", + "Error opening auth tab:", chrome.runtime.lastError ); } - chrome.tabs.create( - { - url: `${onyxDomain}/auth/login`, - active: true, - }, - (_) => { - if (chrome.runtime.lastError) { - console.error( - "Error opening auth tab:", - chrome.runtime.lastError - ); - } - } - ); } ); - } - ); + }); + }); }); } } diff --git a/extensions/chrome/src/utils/storage.js b/extensions/chrome/src/utils/storage.js index 8d06193221..d53c33afdd 100644 --- a/extensions/chrome/src/utils/storage.js +++ b/extensions/chrome/src/utils/storage.js @@ -3,22 +3,95 @@ import { CHROME_SPECIFIC_STORAGE_KEYS, } from "./constants.js"; +export function normalizeOnyxDomain(domain) { + if (typeof domain !== "string") return DEFAULT_ONYX_DOMAIN; + const trimmed = domain.trim().replace(/\/+$/, ""); + return trimmed || DEFAULT_ONYX_DOMAIN; +} + +// Managed storage is populated by enterprise policy (see managed_schema.json). +// It is read-only and takes precedence over anything the user sets locally. +async function getManaged(keys) { + if (!chrome.storage.managed) return {}; + try { + return await chrome.storage.managed.get(keys); + } catch (error) { + console.error( + "Failed to read managed storage; using local settings", + error + ); + return {}; + } +} + +export async function getManagedSettings() { + return getManaged([ + CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN, + CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB, + ]); +} + +export async function isDomainManaged() { + const managed = await getManaged(CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN); + return typeof managed[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] === "string"; +} + +export async function isNewTabOverrideManaged() { + const managed = await getManaged( + CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB + ); + return ( + typeof managed[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB] === + "boolean" + ); +} + export async function getOnyxDomain() { + const managed = await getManaged(CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN); + if (typeof managed[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] === "string") { + return normalizeOnyxDomain( + managed[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN] + ); + } const result = await chrome.storage.local.get({ [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: DEFAULT_ONYX_DOMAIN, }); - return result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]; + return normalizeOnyxDomain(result[CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]); } export function setOnyxDomain(domain, callback) { chrome.storage.local.set( - { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: domain }, + { [CHROME_SPECIFIC_STORAGE_KEYS.ONYX_DOMAIN]: normalizeOnyxDomain(domain) }, callback ); } -export function getOnyxDomainSync() { - return new Promise((resolve) => { - getOnyxDomain(resolve); +// Returns undefined when the user has never chosen, so callers can fall back +// to their own default. +export async function getUseOnyxAsDefaultNewTab() { + const managed = await getManaged( + CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB + ); + const managedValue = + managed[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; + if (typeof managedValue === "boolean") return managedValue; + const result = await chrome.storage.local.get( + CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB + ); + return result[CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]; +} + +export async function setUseOnyxAsDefaultNewTab(value) { + if (await isNewTabOverrideManaged()) return false; + await chrome.storage.local.set({ + [CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB]: !!value, }); + return true; +} + +export function isNewTabOverrideChange(changes, namespace) { + return ( + (namespace === "local" || namespace === "managed") && + CHROME_SPECIFIC_STORAGE_KEYS.USE_ONYX_AS_DEFAULT_NEW_TAB in changes + ); }