-
-
Notifications
You must be signed in to change notification settings - Fork 144
[IMP] add server settings for CORS #795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(), | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+68
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| 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> | ||||||||||||||||||||||||||||||||||||||
| 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; | ||
| } | ||
|
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; | ||
| } | ||
| } | ||
| } | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The store sets
this.errorwhenload()fails, but the component only mapsconfigandloadingfrom the store —erroris never observed. When the API returns an error,loadingbecomesfalseandconfigstaysnull, 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-emptyeditable(cors: [], cors_regex: [], ...) to the server, potentially wiping the existing CORS configuration.Two changes are needed: (1) also map
errorfrom the store and display it, and (2) disable OK when the config hasn't loaded:And update the modal's
ok-disabledbinding: