Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions src/components/CorsConfigModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<template lang="pug">
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 <b>stop and restart the server</b> 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 <code>config.toml</code>. 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 <code>config.toml</code>. 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 <code>config.toml</code>. 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 <code>config.toml</code>. 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 <code>about:debugging#/runtime/this-firefox</code> in your browser.
li Look for your extension and copy the <b>Manifest URL</b> (e.g. <code>moz-extension://4b931c07dededdedff152/manifest.json</code>).
li Remove <code>manifest.json</code> from the end (to get <code>moz-extension://4b931c07dededdedff152</code>).
li Paste it into the <b>Regex CORS origins</b> 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 }}
</template>

<script lang="ts">
import { useCorsStore, type CorsConfig } from '~/stores/cors';
import { mapState } from 'pinia';

export default {
name: 'CorsConfigModal',
data() {
return {
editable: {
cors: [] as string[],
cors_regex: [] as string[],
cors_allow_aw_chrome_extension: false,
cors_allow_all_mozilla_extension: false,
in_file: [] as string[],
needs_restart: false,
} as CorsConfig,
corsStr: '',
corsRegexStr: '',
corsStore: useCorsStore(),
};
},
computed: {
...mapState(useCorsStore, ['config', 'loading', 'error']),
},
watch: {
config(newVal) {
if (newVal) {
this.editable = JSON.parse(JSON.stringify(newVal));
this.corsStr = newVal.cors.join(', ');
this.corsRegexStr = newVal.cors_regex.join(', ');
}
},
},
methods: {
isFixed(field: string): boolean {
return this.config?.in_file?.includes(field) || false;
},
async load() {
await this.corsStore.load();
},
async save(bvModalEvt: any) {
bvModalEvt.preventDefault();

// Parse comma-separated strings back to arrays
this.editable.cors = this.corsStr.split(',').map(s => s.trim()).filter(s => s !== '');
this.editable.cors_regex = this.corsRegexStr.split(',').map(s => s.trim()).filter(s => s !== '');

try {
await this.corsStore.save(this.editable);
(this as any).$bvModal.hide('cors-config-modal');
alert('CORS configuration saved! Please restart the server to apply changes.');
} catch (e: any) {
const msg = e.response?.data?.message || e.message || 'Unknown error';
alert('Failed to save: ' + msg);
}
},
},
};
</script>
67 changes: 67 additions & 0 deletions src/stores/cors.ts
Original file line number Diff line number Diff line change
@@ -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<CorsConfig, 'cors' | 'cors_regex' | 'cors_allow_aw_chrome_extension' | 'cors_allow_all_mozilla_extension'>;

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, needs_restart: true };
}
} catch (e: any) {
this.error = e.response?.data?.message || e.message || 'Failed to save CORS config';
throw e;
} finally {
this.loading = false;
}
}
}
});
3 changes: 0 additions & 3 deletions src/stores/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ interface State {
useMultidevice: boolean;
requestTimeout: number;
// Server configuration
cors_origins: string;

// Set to true if settings loaded
_loaded: boolean;
}
Expand Down Expand Up @@ -85,7 +83,6 @@ export const useSettingsStore = defineStore('settings', {
showYearly: false,
useMultidevice: false,
requestTimeout: 30,
cors_origins: '',

_loaded: false,
}),
Expand Down
33 changes: 0 additions & 33 deletions src/views/settings/ServerSettings.vue

This file was deleted.

11 changes: 6 additions & 5 deletions src/views/settings/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ div
hr

DeveloperSettings
div.mt-2
b-btn(v-b-modal.cors-config-modal, variant="outline-primary", size="sm")
| Configure CORS

hr

ServerSettings
CorsConfigModal
</template>

<script lang="ts">
Expand All @@ -53,7 +54,7 @@ import ReleaseNotificationSettings from '~/views/settings/ReleaseNotificationSet
import CategorizationSettings from '~/views/settings/CategorizationSettings.vue';
import LandingPageSettings from '~/views/settings/LandingPageSettings.vue';
import DeveloperSettings from '~/views/settings/DeveloperSettings.vue';
import ServerSettings from '~/views/settings/ServerSettings.vue';
import CorsConfigModal from '~/components/CorsConfigModal.vue';
import Theme from '~/views/settings/Theme.vue';
import ColorSettings from '~/views/settings/ColorSettings.vue';
import ActivePatternSettings from '~/views/settings/ActivePatternSettings.vue';
Expand All @@ -69,8 +70,8 @@ export default {
Theme,
ColorSettings,
DeveloperSettings,
ServerSettings,
ActivePatternSettings,
CorsConfigModal,
},
beforeRouteLeave(to, from, next) {
const categoryStore = useCategoryStore();
Expand Down