1
0
mirror of https://github.com/amir20/dozzle.git synced 2025-12-24 06:28:42 +01:00

feat: syncs settings to disk for authenticated users (#2445)

This commit is contained in:
Amir Raminfar
2023-10-27 12:53:42 -07:00
committed by GitHub
parent e4d1a18d37
commit 1fe46e605a
19 changed files with 288 additions and 101 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
certs certs
data
dist dist
node_modules node_modules
.cache .cache

View File

@@ -12,14 +12,14 @@ declare global {
const $ref: typeof import('vue/macros')['$ref'] const $ref: typeof import('vue/macros')['$ref']
const $shallowRef: typeof import('vue/macros')['$shallowRef'] const $shallowRef: typeof import('vue/macros')['$shallowRef']
const $toRef: typeof import('vue/macros')['$toRef'] const $toRef: typeof import('vue/macros')['$toRef']
const DEFAULT_SETTINGS: typeof import('./composables/settings')['DEFAULT_SETTINGS'] const DEFAULT_SETTINGS: typeof import('./stores/settings')['DEFAULT_SETTINGS']
const EffectScope: typeof import('vue')['EffectScope'] const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const arrayEquals: typeof import('./utils/index')['arrayEquals'] const arrayEquals: typeof import('./utils/index')['arrayEquals']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const automaticRedirect: typeof import('./composables/settings')['automaticRedirect'] const automaticRedirect: typeof import('./stores/settings')['automaticRedirect']
const collapseNav: typeof import('./composables/settings')['collapseNav'] const collapseNav: typeof import('./stores/settings')['collapseNav']
const computed: typeof import('vue')['computed'] const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync'] const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager'] const computedEager: typeof import('@vueuse/core')['computedEager']
@@ -58,7 +58,7 @@ declare global {
const getDeep: typeof import('./utils/index')['getDeep'] const getDeep: typeof import('./utils/index')['getDeep']
const globalShowPopup: typeof import('./composables/popup')['globalShowPopup'] const globalShowPopup: typeof import('./composables/popup')['globalShowPopup']
const h: typeof import('vue')['h'] const h: typeof import('vue')['h']
const hourStyle: typeof import('./composables/settings')['hourStyle'] const hourStyle: typeof import('./stores/settings')['hourStyle']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject'] const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal'] const injectLocal: typeof import('@vueuse/core')['injectLocal']
@@ -69,7 +69,7 @@ declare global {
const isReactive: typeof import('vue')['isReactive'] const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly'] const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef'] const isRef: typeof import('vue')['isRef']
const lightTheme: typeof import('./composables/settings')['lightTheme'] const lightTheme: typeof import('./stores/settings')['lightTheme']
const logicAnd: typeof import('@vueuse/math')['logicAnd'] const logicAnd: typeof import('@vueuse/math')['logicAnd']
const logicNot: typeof import('@vueuse/math')['logicNot'] const logicNot: typeof import('@vueuse/math')['logicNot']
const logicOr: typeof import('@vueuse/math')['logicOr'] const logicOr: typeof import('@vueuse/math')['logicOr']
@@ -80,7 +80,7 @@ declare global {
const mapStores: typeof import('pinia')['mapStores'] const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState'] const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw'] const markRaw: typeof import('vue')['markRaw']
const menuWidth: typeof import('./composables/settings')['menuWidth'] const menuWidth: typeof import('./stores/settings')['menuWidth']
const nextTick: typeof import('vue')['nextTick'] const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated'] const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount'] const onBeforeMount: typeof import('vue')['onBeforeMount']
@@ -123,21 +123,21 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent'] const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const search: typeof import('./composables/settings')['search'] const search: typeof import('./stores/settings')['search']
const sessionHost: typeof import('./composables/storage')['sessionHost'] const sessionHost: typeof import('./composables/storage')['sessionHost']
const setActivePinia: typeof import('pinia')['setActivePinia'] const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setTitle: typeof import('./composables/title')['setTitle'] const setTitle: typeof import('./composables/title')['setTitle']
const settings: typeof import('./composables/settings')['settings'] const settings: typeof import('./stores/settings')['settings']
const shallowReactive: typeof import('vue')['shallowReactive'] const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef'] const shallowRef: typeof import('vue')['shallowRef']
const showAllContainers: typeof import('./composables/settings')['showAllContainers'] const showAllContainers: typeof import('./stores/settings')['showAllContainers']
const showStd: typeof import('./composables/settings')['showStd'] const showStd: typeof import('./stores/settings')['showStd']
const showTimestamp: typeof import('./composables/settings')['showTimestamp'] const showTimestamp: typeof import('./stores/settings')['showTimestamp']
const size: typeof import('./composables/settings')['size'] const size: typeof import('./stores/settings')['size']
const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars'] const smallerScrollbars: typeof import('./stores/settings')['smallerScrollbars']
const softWrap: typeof import('./composables/settings')['softWrap'] const softWrap: typeof import('./stores/settings')['softWrap']
const storeToRefs: typeof import('pinia')['storeToRefs'] const storeToRefs: typeof import('pinia')['storeToRefs']
const stripVersion: typeof import('./utils/index')['stripVersion'] const stripVersion: typeof import('./utils/index')['stripVersion']
const syncRef: typeof import('@vueuse/core')['syncRef'] const syncRef: typeof import('@vueuse/core')['syncRef']
@@ -358,6 +358,7 @@ declare global {
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever'] const whenever: typeof import('@vueuse/core')['whenever']
const withBase: typeof import('./stores/config')['withBase']
} }
// for type re-export // for type re-export
declare global { declare global {
@@ -375,14 +376,14 @@ declare module 'vue' {
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']> readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']> readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']> readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']> readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']> readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']> readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']> readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']> readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']> readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly automaticRedirect: UnwrapRef<typeof import('./composables/settings')['automaticRedirect']> readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']> readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
readonly computed: UnwrapRef<typeof import('vue')['computed']> readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']> readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']> readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
@@ -419,7 +420,7 @@ declare module 'vue' {
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']> readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']> readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']> readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']> readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']> readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']> readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']> readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
@@ -430,7 +431,7 @@ declare module 'vue' {
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']> readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']> readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']> readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']> readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']> readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']> readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -438,7 +439,7 @@ declare module 'vue' {
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']> readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']> readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']> readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']> readonly menuWidth: UnwrapRef<typeof import('./stores/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']> readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']> readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']> readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
@@ -481,21 +482,21 @@ declare module 'vue' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']> readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']> readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']> readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']> readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']> readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']> readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']> readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']> readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']> readonly settings: UnwrapRef<typeof import('./stores/settings')['settings']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']> readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']> readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']> readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']> readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']> readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']> readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']> readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']> readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']> readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']> readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']> readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']> readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
@@ -703,6 +704,7 @@ declare module 'vue' {
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']> readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']> readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']> readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
} }
} }
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
@@ -714,14 +716,14 @@ declare module '@vue/runtime-core' {
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']> readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']> readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']> readonly $toRef: UnwrapRef<typeof import('vue/macros')['$toRef']>
readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./composables/settings')['DEFAULT_SETTINGS']> readonly DEFAULT_SETTINGS: UnwrapRef<typeof import('./stores/settings')['DEFAULT_SETTINGS']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']> readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']> readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']> readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']> readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']> readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly automaticRedirect: UnwrapRef<typeof import('./composables/settings')['automaticRedirect']> readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']> readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
readonly computed: UnwrapRef<typeof import('vue')['computed']> readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']> readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']> readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
@@ -758,7 +760,7 @@ declare module '@vue/runtime-core' {
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']> readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']> readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
readonly h: UnwrapRef<typeof import('vue')['h']> readonly h: UnwrapRef<typeof import('vue')['h']>
readonly hourStyle: UnwrapRef<typeof import('./composables/settings')['hourStyle']> readonly hourStyle: UnwrapRef<typeof import('./stores/settings')['hourStyle']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']> readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']> readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']> readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
@@ -769,7 +771,7 @@ declare module '@vue/runtime-core' {
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']> readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']> readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']> readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly lightTheme: UnwrapRef<typeof import('./composables/settings')['lightTheme']> readonly lightTheme: UnwrapRef<typeof import('./stores/settings')['lightTheme']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']> readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']> readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']> readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
@@ -777,7 +779,7 @@ declare module '@vue/runtime-core' {
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']> readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']> readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']> readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly menuWidth: UnwrapRef<typeof import('./composables/settings')['menuWidth']> readonly menuWidth: UnwrapRef<typeof import('./stores/settings')['menuWidth']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']> readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']> readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']> readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
@@ -820,21 +822,21 @@ declare module '@vue/runtime-core' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']> readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']> readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']> readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly search: UnwrapRef<typeof import('./composables/settings')['search']> readonly search: UnwrapRef<typeof import('./stores/settings')['search']>
readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']> readonly sessionHost: UnwrapRef<typeof import('./composables/storage')['sessionHost']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']> readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']> readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']> readonly setTitle: UnwrapRef<typeof import('./composables/title')['setTitle']>
readonly settings: UnwrapRef<typeof import('./composables/settings')['settings']> readonly settings: UnwrapRef<typeof import('./stores/settings')['settings']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']> readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']> readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']> readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']> readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']> readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']> readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
readonly size: UnwrapRef<typeof import('./composables/settings')['size']> readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']> readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']> readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']> readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']> readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']> readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
@@ -1042,5 +1044,6 @@ declare module '@vue/runtime-core' {
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']> readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']> readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']> readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
} }
} }

View File

@@ -4,7 +4,7 @@ import { createTestingPinia } from "@pinia/testing";
import EventSource, { sources } from "eventsourcemock"; import EventSource, { sources } from "eventsourcemock";
import LogEventSource from "./LogEventSource.vue"; import LogEventSource from "./LogEventSource.vue";
import LogViewer from "./LogViewer.vue"; import LogViewer from "./LogViewer.vue";
import { settings } from "@/composables/settings"; import { settings } from "@/stores/settings";
import { useSearchFilter } from "@/composables/search"; import { useSearchFilter } from "@/composables/search";
import { vi, describe, expect, beforeEach, test, afterEach } from "vitest"; import { vi, describe, expect, beforeEach, test, afterEach } from "vitest";
import { computed, nextTick } from "vue"; import { computed, nextTick } from "vue";
@@ -14,6 +14,7 @@ import { containerContext } from "@/composables/containerContext";
vi.mock("@/stores/config", () => ({ vi.mock("@/stores/config", () => ({
__esModule: true, __esModule: true,
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] }, default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
withBase: (path: string) => path,
})); }));
/** /**

View File

@@ -85,7 +85,7 @@ export function useLogStream() {
console.debug(`Connecting to ${containerId} with params`, params); console.debug(`Connecting to ${containerId} with params`, params);
es = new EventSource( es = new EventSource(
`${config.base}/api/logs/stream/${container.value.host}/${containerId}?${new URLSearchParams(params).toString()}`, withBase(`/api/logs/stream/${container.value.host}/${containerId}?${new URLSearchParams(params).toString()}`),
); );
es.addEventListener("container-stopped", () => { es.addEventListener("container-stopped", () => {
close(); close();
@@ -118,7 +118,7 @@ export function useLogStream() {
const logs = await ( const logs = await (
await fetch( await fetch(
`${config.base}/api/logs/${container.value.host}/${containerId}?${new URLSearchParams(params).toString()}`, withBase(`/api/logs/${container.value.host}/${containerId}?${new URLSearchParams(params).toString()}`),
) )
).text(); ).text();
if (logs) { if (logs) {

View File

@@ -65,7 +65,7 @@
<script lang="ts" setup> <script lang="ts" setup>
// @ts-ignore - splitpanes types are not available // @ts-ignore - splitpanes types are not available
import { Splitpanes, Pane } from "splitpanes"; import { Splitpanes, Pane } from "splitpanes";
import { collapseNav } from "@/composables/settings"; import { collapseNav } from "@/stores/settings";
const { authorizationNeeded } = config; const { authorizationNeeded } = config;
const containerStore = useContainerStore(); const containerStore = useContainerStore();

View File

@@ -2,13 +2,12 @@ import { type App } from "vue";
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import pages from "~pages"; import pages from "~pages";
import { setupLayouts } from "virtual:generated-layouts"; import { setupLayouts } from "virtual:generated-layouts";
import config from "@/stores/config";
export const install = (app: App) => { export const install = (app: App) => {
const routes = setupLayouts(pages); const routes = setupLayouts(pages);
const router = createRouter({ const router = createRouter({
history: createWebHistory(`${config.base}/`), history: createWebHistory(withBase("/")),
routes, routes,
}); });

View File

@@ -49,14 +49,14 @@ let password = $ref("");
let form: HTMLFormElement | undefined = $ref(); let form: HTMLFormElement | undefined = $ref();
async function onLogin() { async function onLogin() {
const response = await fetch(`${config.base}/api/validateCredentials`, { const response = await fetch(withBase("/api/validateCredentials"), {
body: new FormData(form), body: new FormData(form),
method: "post", method: "post",
}); });
if (response.status == 200) { if (response.status == 200) {
error = false; error = false;
window.location.href = `${config.base}/`; window.location.href = withBase("/");
} else { } else {
error = true; error = true;
} }

View File

@@ -100,7 +100,7 @@ import {
size, size,
softWrap, softWrap,
automaticRedirect, automaticRedirect,
} from "@/composables/settings"; } from "@/stores/settings";
const { t } = useI18n(); const { t } = useI18n();

View File

@@ -1,3 +1,5 @@
import { type Settings } from "@/stores/settings";
const text = document.querySelector("script#config__json")?.textContent || "{}"; const text = document.querySelector("script#config__json")?.textContent || "{}";
interface Config { interface Config {
@@ -14,6 +16,7 @@ interface Config {
name: string; name: string;
avatar: string; avatar: string;
}; };
serverSettings?: Settings;
} }
const pageConfig = JSON.parse(text); const pageConfig = JSON.parse(text);
@@ -25,4 +28,6 @@ const config: Config = {
config.version = config.version.replace(/^v/, ""); config.version = config.version.replace(/^v/, "");
export default config; export default Object.freeze(config);
export const withBase = (path: string) => `${config.base}${path}`;

View File

@@ -34,7 +34,7 @@ export const useContainerStore = defineStore("container", () => {
function connect() { function connect() {
es?.close(); es?.close();
ready.value = false; ready.value = false;
es = new EventSource(`${config.base}/api/events/stream`); es = new EventSource(withBase("/api/events/stream"));
es.addEventListener("error", (e) => { es.addEventListener("error", (e) => {
if (es?.readyState === EventSource.CLOSED) { if (es?.readyState === EventSource.CLOSED) {
showToast( showToast(

View File

@@ -1,7 +1,7 @@
import { toRefs } from "@vueuse/core"; import { toRefs } from "@vueuse/core";
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS"; const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
export const DEFAULT_SETTINGS: { export type Settings = {
search: boolean; search: boolean;
size: "small" | "medium" | "large"; size: "small" | "medium" | "large";
menuWidth: number; menuWidth: number;
@@ -14,7 +14,8 @@ export const DEFAULT_SETTINGS: {
softWrap: boolean; softWrap: boolean;
collapseNav: boolean; collapseNav: boolean;
automaticRedirect: boolean; automaticRedirect: boolean;
} = { };
export const DEFAULT_SETTINGS: Settings = {
search: true, search: true,
size: "medium", size: "medium",
menuWidth: 15, menuWidth: 15,
@@ -30,7 +31,14 @@ export const DEFAULT_SETTINGS: {
}; };
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS); export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
settings.value = { ...DEFAULT_SETTINGS, ...settings.value }; settings.value = { ...DEFAULT_SETTINGS, ...settings.value, ...config.serverSettings };
watch(settings, (value) => {
fetch(withBase("/api/profile/settings"), {
method: "PUT",
body: JSON.stringify(value),
});
});
export const { export const {
collapseNav, collapseNav,

View File

@@ -6,13 +6,11 @@ import (
"encoding/hex" "encoding/hex"
"net/http" "net/http"
"strings" "strings"
log "github.com/sirupsen/logrus"
) )
type contextKey string type contextKey string
const RemoteUser contextKey = "remoteUser" const remoteUser contextKey = "remoteUser"
type User struct { type User struct {
Username string `json:"username"` Username string `json:"username"`
@@ -44,15 +42,21 @@ func newUser(username, email, name string) *User {
func ForwardProxyAuthorizationRequired(next http.Handler) http.Handler { func ForwardProxyAuthorizationRequired(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Remote-Email") == "" { if r.Header.Get("Remote-Email") != "" {
log.Error("Unable to find remote email. Please check your proxy configuration. Expecting header 'Remote-Email'") user := newUser(r.Header.Get("Remote-User"), r.Header.Get("Remote-Email"), r.Header.Get("Remote-Name"))
http.Error(w, "Unauthorized", http.StatusUnauthorized) ctx := context.WithValue(r.Context(), remoteUser, user)
return next.ServeHTTP(w, r.WithContext(ctx))
} else {
next.ServeHTTP(w, r)
} }
user := newUser(r.Header.Get("Remote-User"), r.Header.Get("Remote-Email"), r.Header.Get("Remote-Name"))
ctx := context.WithValue(r.Context(), RemoteUser, user)
next.ServeHTTP(w, r.WithContext(ctx))
}) })
} }
func RemoteUserFromContext(ctx context.Context) *User {
user, ok := ctx.Value(remoteUser).(*User)
if !ok {
return nil
}
return user
}

View File

@@ -0,0 +1,99 @@
package profile
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"github.com/amir20/dozzle/internal/auth"
log "github.com/sirupsen/logrus"
)
type Settings struct {
Search bool `json:"search"`
MenuWidth float32 `json:"menuWidth"`
SmallerScrollbars bool `json:"smallerScrollbars"`
ShowTimestamp bool `json:"showTimestamp"`
ShowStd bool `json:"showStd"`
ShowAllContainers bool `json:"showAllContainers"`
SoftWrap bool `json:"softWrap"`
CollapseNav bool `json:"collapseNav"`
AutomaticRedirect bool `json:"automaticRedirect"`
Size string `json:"size,omitempty"`
LightTheme string `json:"lightTheme,omitempty"`
HourStyle string `json:"hourStyle,omitempty"`
}
var data_path string
func init() {
path, err := filepath.Abs("./data")
if err != nil {
log.Fatalf("Unable to get absolute path for data directory: %s", err)
return
}
if _, err := os.Stat(path); os.IsNotExist(err) {
if err := os.Mkdir(path, 0755); err != nil {
log.Fatalf("Unable to create data directory: %s", err)
return
}
}
data_path = path
}
func SaveUserSettings(user *auth.User, settings *Settings) error {
path := filepath.Join(data_path, user.Username)
// Create user directory if it doesn't exist
if _, err := os.Stat(path); os.IsNotExist(err) {
if err := os.Mkdir(path, 0755); err != nil {
return err
}
}
settings_path := filepath.Join(path, "settings.json")
data, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
f, err := os.Create(settings_path)
if err != nil {
return err
}
defer f.Close()
if _, err := f.Write(data); err != nil {
return err
}
log.Debugf("Saved settings for user %s", user.Username)
return f.Sync()
}
func LoadUserSettings(user *auth.User) (*Settings, error) {
path := filepath.Join(data_path, user.Username)
settings_path := filepath.Join(path, "settings.json")
if _, err := os.Stat(settings_path); os.IsNotExist(err) {
return &Settings{}, errors.New("Settings file does not exist")
}
f, err := os.Open(settings_path)
if err != nil {
return nil, err
}
defer f.Close()
var settings Settings
if err := json.NewDecoder(f).Decode(&settings); err != nil {
return nil, err
}
return &settings, nil
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/amir20/dozzle/internal/analytics" "github.com/amir20/dozzle/internal/analytics"
"github.com/amir20/dozzle/internal/auth" "github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/docker" "github.com/amir20/dozzle/internal/docker"
"github.com/amir20/dozzle/internal/profile"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -21,39 +22,12 @@ func (h *handler) index(w http.ResponseWriter, req *http.Request) {
_, err := h.content.Open(req.URL.Path) _, err := h.content.Open(req.URL.Path)
if err == nil && req.URL.Path != "" && req.URL.Path != "/" { if err == nil && req.URL.Path != "" && req.URL.Path != "/" {
fileServer.ServeHTTP(w, req) fileServer.ServeHTTP(w, req)
if !h.config.NoAnalytics {
go func() {
host, _ := os.Hostname()
var client DockerClient
for _, v := range h.clients {
client = v
break
}
if containers, err := client.ListContainers(); err == nil {
totalContainers := len(containers)
runningContainers := 0
for _, container := range containers {
if container.State == "running" {
runningContainers++
}
}
re := analytics.RequestEvent{
ClientId: host,
TotalContainers: totalContainers,
RunningContainers: runningContainers,
}
analytics.SendRequestEvent(re)
}
}()
}
} else { } else {
if !isAuthorized(req) && req.URL.Path != "login" { if !isAuthorized(req) && req.URL.Path != "login" {
http.Redirect(w, req, path.Clean(h.config.Base+"/login"), http.StatusTemporaryRedirect) http.Redirect(w, req, path.Clean(h.config.Base+"/login"), http.StatusTemporaryRedirect)
return return
} }
go h.sendRequestEvent()
h.executeTemplate(w, req) h.executeTemplate(w, req)
} }
} }
@@ -99,9 +73,23 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
"hosts": hosts, "hosts": hosts,
} }
if h.config.AuthProvider == "forward-proxy" { if h.config.AuthProvider == FORWARD_PROXY {
user := req.Context().Value(auth.RemoteUser).(*auth.User) user := auth.RemoteUserFromContext(req.Context())
if user == nil {
log.Error("Unable to find remote user. Please check your proxy configuration. Expecting headers Remote-Email, Remote-User, Remote-Name.")
log.Debugf("Dumping all headers for url /%s", req.URL.String())
for k, v := range req.Header {
log.Debugf("%s: %s", k, v)
}
http.Error(w, "Unauthorized user", http.StatusUnauthorized)
return
}
config["user"] = user config["user"] = user
if settings, err := profile.LoadUserSettings(user); err == nil {
config["serverSettings"] = settings
} else {
config["serverSettings"] = struct{}{}
}
} }
data := map[string]interface{}{ data := map[string]interface{}{
@@ -138,5 +126,33 @@ func (h *handler) readManifest() map[string]interface{} {
} }
return manifest return manifest
} }
}
func (h *handler) sendRequestEvent() {
if !h.config.NoAnalytics {
host, _ := os.Hostname()
var client DockerClient
for _, v := range h.clients {
client = v
break
}
if containers, err := client.ListContainers(); err == nil {
totalContainers := len(containers)
runningContainers := 0
for _, container := range containers {
if container.State == "running" {
runningContainers++
}
}
re := analytics.RequestEvent{
ClientId: host,
TotalContainers: totalContainers,
RunningContainers: runningContainers,
}
analytics.SendRequestEvent(re)
}
}
} }

32
internal/web/profile.go Normal file
View File

@@ -0,0 +1,32 @@
package web
import (
"encoding/json"
"net/http"
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/profile"
log "github.com/sirupsen/logrus"
)
func (h *handler) saveSettings(w http.ResponseWriter, r *http.Request) {
var settings profile.Settings
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
user := auth.RemoteUserFromContext(r.Context())
if user == nil {
http.Error(w, "Unable to find user", http.StatusInternalServerError)
return
}
if err := profile.SaveUserSettings(user, &settings); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Errorf("Unable to save user settings: %s", err)
return
}
w.WriteHeader(http.StatusOK)
}

View File

@@ -16,6 +16,13 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type AuthProvider string
const (
SIMPLE AuthProvider = "simple"
FORWARD_PROXY AuthProvider = "forward-proxy"
)
// Config is a struct for configuring the web service // Config is a struct for configuring the web service
type Config struct { type Config struct {
Base string Base string
@@ -26,7 +33,7 @@ type Config struct {
Hostname string Hostname string
NoAnalytics bool NoAnalytics bool
Dev bool Dev bool
AuthProvider string AuthProvider AuthProvider
} }
type handler struct { type handler struct {
@@ -70,7 +77,7 @@ func createRouter(h *handler) *chi.Mux {
r.Route(base, func(r chi.Router) { r.Route(base, func(r chi.Router) {
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
if h.config.AuthProvider == "forward-proxy" { if h.config.AuthProvider == FORWARD_PROXY {
r.Use(auth.ForwardProxyAuthorizationRequired) r.Use(auth.ForwardProxyAuthorizationRequired)
} }
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
@@ -81,6 +88,7 @@ func createRouter(h *handler) *chi.Mux {
r.Get("/api/events/stream", h.streamEvents) r.Get("/api/events/stream", h.streamEvents)
r.Get("/logout", h.clearSession) r.Get("/logout", h.clearSession)
r.Get("/version", h.version) r.Get("/version", h.version)
r.Put("/api/profile/settings", h.saveSettings)
}) })
defaultHandler := http.StripPrefix(strings.Replace(base+"/", "//", "/", 1), http.HandlerFunc(h.index)) defaultHandler := http.StripPrefix(strings.Replace(base+"/", "//", "/", 1), http.HandlerFunc(h.index))

10
main.go
View File

@@ -172,6 +172,14 @@ func createClients(args args,
func createServer(args args, clients map[string]web.DockerClient) *http.Server { func createServer(args args, clients map[string]web.DockerClient) *http.Server {
_, dev := os.LookupEnv("DEV") _, dev := os.LookupEnv("DEV")
var provider web.AuthProvider
if args.AuthProvider == "forward-proxy" {
provider = web.FORWARD_PROXY
} else if args.AuthProvider == "simple" {
provider = web.SIMPLE
}
config := web.Config{ config := web.Config{
Addr: args.Addr, Addr: args.Addr,
Base: args.Base, Base: args.Base,
@@ -181,7 +189,7 @@ func createServer(args args, clients map[string]web.DockerClient) *http.Server {
Hostname: args.Hostname, Hostname: args.Hostname,
NoAnalytics: args.NoAnalytics, NoAnalytics: args.NoAnalytics,
Dev: dev, Dev: dev,
AuthProvider: args.AuthProvider, AuthProvider: provider,
} }
assets, err := fs.Sub(content, "dist") assets, err := fs.Sub(content, "dist")

0
public/favicon.ico Normal file
View File

View File

@@ -65,6 +65,9 @@ export default defineConfig(() => ({
}), }),
], ],
server: { server: {
watch: {
ignored: ["**/data/**"],
},
proxy: { proxy: {
"/api": { "/api": {
target: { target: {