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...
Comment on lines +43 to +44
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing error state — OK button stays active on load failure

The store sets this.error when load() fails, but the component only maps config and loading from the store — error is never observed. When the API returns an error, loading becomes false and config stays null, so the modal shows a completely blank body while the OK button remains enabled (:ok-disabled="loading" only guards the in-flight case). A user clicking OK at that point sends the component's initialised-to-empty editable (cors: [], cors_regex: [], ...) to the server, potentially wiping the existing CORS configuration.

Two changes are needed: (1) also map error from the store and display it, and (2) disable OK when the config hasn't loaded:

Suggested change
div(v-else-if="loading")
p Loading...
div(v-else-if="loading")
p Loading...
div(v-else-if="error")
b-alert(show variant="danger") Failed to load CORS configuration: {{ error }}

And update the modal's ok-disabled binding:

b-modal(... :ok-disabled="loading || !config" ...)

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(),
};
Comment on lines +57 to +68
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 editable declared type is missing CorsConfig fields

editable is initialised with only four fields, but corsStore.save() expects a full CorsConfig (which also requires in_file and needs_restart). TypeScript will likely raise a compile error at the corsStore.save(this.editable) call because the declared type of editable does not satisfy CorsConfig. Declare editable as CorsConfig from the start so the types align end-to-end.

Suggested change
editable: {
cors: [] as string[],
cors_regex: [] as string[],
cors_allow_aw_chrome_extension: false,
cors_allow_all_mozilla_extension: false,
},
corsStr: '',
corsRegexStr: '',
corsStore: useCorsStore(),
};
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,

},
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;
}
Comment thread
RaoufGhrissi marked this conversation as resolved.
},
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;
}
}
}
});
7 changes: 7 additions & 0 deletions src/views/settings/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
</template>

<script lang="ts">
Expand All @@ -49,6 +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 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 @@ -65,6 +71,7 @@ export default {
ColorSettings,
DeveloperSettings,
ActivePatternSettings,
CorsConfigModal,
},
beforeRouteLeave(to, from, next) {
const categoryStore = useCategoryStore();
Expand Down