diff --git a/src/components/CorsConfigModal.vue b/src/components/CorsConfigModal.vue
new file mode 100644
index 00000000..bb93ac6b
--- /dev/null
+++ b/src/components/CorsConfigModal.vue
@@ -0,0 +1,107 @@
+
+b-modal(id="cors-config-modal" title="Security & CORS" @show="load" @ok="save" :ok-disabled="loading || !config" size="lg")
+ div(v-if="config")
+ b-alert(v-if="config.needs_restart" show variant="warning" class="mb-4")
+ h5.alert-heading ⚠️ Server Restart Required
+ p.mb-0
+ | CORS settings are only applied once at startup. You must stop and restart the server for any changes made here to take effect.
+
+ b-form-group(label="Fixed CORS origins" label-cols-md=4 description="Configure general CORS origins with exact matches (e.g. http://localhost:8080). Comma-separated.")
+ b-input(v-model="corsStr" type="text" :disabled="isFixed('cors')")
+ small.text-warning(v-if="isFixed('cors')")
+ | ⚠️ Fixed in config.toml. Settings in the configuration file take precedence and cannot be changed here.
+
+ b-form-group(label="Regex CORS origins" label-cols-md=4 description="Configure CORS origins with regular expressions. Useful for browser extensions (e.g. chrome-extension://.* or moz-extension://.*). Comma-separated.")
+ b-input(v-model="corsRegexStr" type="text" :disabled="isFixed('cors_regex')")
+ small.text-warning(v-if="isFixed('cors_regex')")
+ | ⚠️ Fixed in config.toml. Settings in the configuration file take precedence and cannot be changed here.
+
+ h5.mt-4 Extensions Shortcuts
+ b-form-group(label-cols-md=4)
+ b-form-checkbox(v-model="editable.cors_allow_aw_chrome_extension" :disabled="isFixed('cors_allow_aw_chrome_extension')") Allow ActivityWatch extension (Chrome)
+ template(#description)
+ div Chrome extensions use a stable, persistent ID, so the official extension is reliably supported.
+ small.text-warning(v-if="isFixed('cors_allow_aw_chrome_extension')")
+ | ⚠️ Fixed in config.toml. Settings in the configuration file take precedence and cannot be changed here.
+
+ b-form-group(label-cols-md=4)
+ b-form-checkbox(v-model="editable.cors_allow_all_mozilla_extension" :disabled="isFixed('cors_allow_all_mozilla_extension')") Allow all Firefox extensions (DANGEROUS)
+ template(#description)
+ div Every version of a Mozilla extension has its own ID to avoid fingerprinting. This is why you must either allow all extensions or manually configure your specific ID.
+ small.text-warning.mb-2.d-block(v-if="isFixed('cors_allow_all_mozilla_extension')")
+ | ⚠️ Fixed in config.toml. Settings in the configuration file take precedence and cannot be changed here.
+ div.mt-2.text-danger(v-if="editable.cors_allow_all_mozilla_extension")
+ | ⚠️ DANGEROUS: Not recommended for security. If enabled, any installed extension can access your ActivityWatch data. Use this only if you know what extensions you have and assume full responsibility.
+ div(v-else)
+ | Recommended for security. To allow a specific extension safely:
+ ol.mt-2.mb-1
+ li Go to about:debugging#/runtime/this-firefox in your browser.
+ li Look for your extension and copy the Manifest URL (e.g. moz-extension://4b931c07dededdedff152/manifest.json).
+ li Remove manifest.json from the end (to get moz-extension://4b931c07dededdedff152).
+ li Paste it into the Regex CORS origins field above (use a comma to separate if not empty).
+
+ div(v-else-if="loading")
+ p Loading...
+ div(v-else-if="error")
+ b-alert(show variant="danger") Failed to load CORS configuration: {{ error }}
+
+
+
diff --git a/src/stores/cors.ts b/src/stores/cors.ts
new file mode 100644
index 00000000..3a13623c
--- /dev/null
+++ b/src/stores/cors.ts
@@ -0,0 +1,67 @@
+import { defineStore } from 'pinia';
+import { getClient } from '~/util/awclient';
+
+export interface CorsConfig {
+ cors: string[];
+ cors_regex: string[];
+ cors_allow_aw_chrome_extension: boolean;
+ cors_allow_all_mozilla_extension: boolean;
+ in_file: string[];
+ needs_restart: boolean;
+}
+
+export type MutableCorsConfig = Pick;
+
+interface State {
+ config: CorsConfig | null;
+ loading: boolean;
+ error: string | null;
+}
+
+export const useCorsStore = defineStore('cors', {
+ state: (): State => ({
+ config: null,
+ loading: false,
+ error: null,
+ }),
+ actions: {
+ async load() {
+ this.loading = true;
+ this.error = null;
+ try {
+ const client = getClient();
+ const response = await client.req.get('/0/cors-config');
+ this.config = response.data;
+ } catch (e: any) {
+ this.error = e.response?.data?.message || e.message || 'Failed to load CORS config';
+ } finally {
+ this.loading = false;
+ }
+ },
+ async save(newConfig: MutableCorsConfig) {
+ this.loading = true;
+ this.error = null;
+ try {
+ const client = getClient();
+ // Only send the mutable subset to the server
+ const payload: MutableCorsConfig = {
+ cors: newConfig.cors,
+ cors_regex: newConfig.cors_regex,
+ cors_allow_aw_chrome_extension: newConfig.cors_allow_aw_chrome_extension,
+ cors_allow_all_mozilla_extension: newConfig.cors_allow_all_mozilla_extension,
+ };
+ await client.req.post('/0/cors-config', payload);
+
+ // Update local state if successful
+ if (this.config) {
+ this.config = { ...this.config, ...payload };
+ }
+ } catch (e: any) {
+ this.error = e.response?.data?.message || e.message || 'Failed to save CORS config';
+ throw e;
+ } finally {
+ this.loading = false;
+ }
+ }
+ }
+});
diff --git a/src/views/settings/Settings.vue b/src/views/settings/Settings.vue
index 0328ad18..2a55989d 100644
--- a/src/views/settings/Settings.vue
+++ b/src/views/settings/Settings.vue
@@ -37,6 +37,11 @@ div
hr
DeveloperSettings
+ div.mt-2
+ b-btn(v-b-modal.cors-config-modal, variant="outline-primary", size="sm")
+ | Configure CORS
+
+ CorsConfigModal