mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 13:23:07 +01:00
feat: syncs settings to disk for authenticated users (#2445)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
certs
|
||||
data
|
||||
dist
|
||||
node_modules
|
||||
.cache
|
||||
|
||||
87
assets/auto-imports.d.ts
vendored
87
assets/auto-imports.d.ts
vendored
@@ -12,14 +12,14 @@ declare global {
|
||||
const $ref: typeof import('vue/macros')['$ref']
|
||||
const $shallowRef: typeof import('vue/macros')['$shallowRef']
|
||||
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 acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const arrayEquals: typeof import('./utils/index')['arrayEquals']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const automaticRedirect: typeof import('./composables/settings')['automaticRedirect']
|
||||
const collapseNav: typeof import('./composables/settings')['collapseNav']
|
||||
const automaticRedirect: typeof import('./stores/settings')['automaticRedirect']
|
||||
const collapseNav: typeof import('./stores/settings')['collapseNav']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
@@ -58,7 +58,7 @@ declare global {
|
||||
const getDeep: typeof import('./utils/index')['getDeep']
|
||||
const globalShowPopup: typeof import('./composables/popup')['globalShowPopup']
|
||||
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 inject: typeof import('vue')['inject']
|
||||
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
||||
@@ -69,7 +69,7 @@ declare global {
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
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 logicNot: typeof import('@vueuse/math')['logicNot']
|
||||
const logicOr: typeof import('@vueuse/math')['logicOr']
|
||||
@@ -80,7 +80,7 @@ declare global {
|
||||
const mapStores: typeof import('pinia')['mapStores']
|
||||
const mapWritableState: typeof import('pinia')['mapWritableState']
|
||||
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 onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
@@ -123,21 +123,21 @@ declare global {
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
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 setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
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 shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const showAllContainers: typeof import('./composables/settings')['showAllContainers']
|
||||
const showStd: typeof import('./composables/settings')['showStd']
|
||||
const showTimestamp: typeof import('./composables/settings')['showTimestamp']
|
||||
const size: typeof import('./composables/settings')['size']
|
||||
const smallerScrollbars: typeof import('./composables/settings')['smallerScrollbars']
|
||||
const softWrap: typeof import('./composables/settings')['softWrap']
|
||||
const showAllContainers: typeof import('./stores/settings')['showAllContainers']
|
||||
const showStd: typeof import('./stores/settings')['showStd']
|
||||
const showTimestamp: typeof import('./stores/settings')['showTimestamp']
|
||||
const size: typeof import('./stores/settings')['size']
|
||||
const smallerScrollbars: typeof import('./stores/settings')['smallerScrollbars']
|
||||
const softWrap: typeof import('./stores/settings')['softWrap']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const stripVersion: typeof import('./utils/index')['stripVersion']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
@@ -358,6 +358,7 @@ declare global {
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
const withBase: typeof import('./stores/config')['withBase']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
@@ -375,14 +376,14 @@ declare module 'vue' {
|
||||
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
|
||||
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
|
||||
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 acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./composables/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
@@ -419,7 +420,7 @@ declare module 'vue' {
|
||||
readonly getDeep: UnwrapRef<typeof import('./utils/index')['getDeep']>
|
||||
readonly globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
|
||||
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 inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
||||
@@ -430,7 +431,7 @@ declare module 'vue' {
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
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 mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
@@ -438,7 +439,7 @@ declare module 'vue' {
|
||||
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
|
||||
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||
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 onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
@@ -481,21 +482,21 @@ declare module 'vue' {
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
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 setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
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 shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
|
||||
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
|
||||
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
|
||||
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
|
||||
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
|
||||
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
|
||||
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
|
||||
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
|
||||
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
|
||||
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
|
||||
readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
|
||||
readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
|
||||
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
|
||||
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
|
||||
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
||||
@@ -703,6 +704,7 @@ declare module 'vue' {
|
||||
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
|
||||
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
|
||||
}
|
||||
}
|
||||
declare module '@vue/runtime-core' {
|
||||
@@ -714,14 +716,14 @@ declare module '@vue/runtime-core' {
|
||||
readonly $ref: UnwrapRef<typeof import('vue/macros')['$ref']>
|
||||
readonly $shallowRef: UnwrapRef<typeof import('vue/macros')['$shallowRef']>
|
||||
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 acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly arrayEquals: UnwrapRef<typeof import('./utils/index')['arrayEquals']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./composables/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./composables/settings')['collapseNav']>
|
||||
readonly automaticRedirect: UnwrapRef<typeof import('./stores/settings')['automaticRedirect']>
|
||||
readonly collapseNav: UnwrapRef<typeof import('./stores/settings')['collapseNav']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
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 globalShowPopup: UnwrapRef<typeof import('./composables/popup')['globalShowPopup']>
|
||||
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 inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
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 isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
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 mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
@@ -777,7 +779,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
|
||||
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
|
||||
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 onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
@@ -820,21 +822,21 @@ declare module '@vue/runtime-core' {
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
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 setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
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 shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly showAllContainers: UnwrapRef<typeof import('./composables/settings')['showAllContainers']>
|
||||
readonly showStd: UnwrapRef<typeof import('./composables/settings')['showStd']>
|
||||
readonly showTimestamp: UnwrapRef<typeof import('./composables/settings')['showTimestamp']>
|
||||
readonly size: UnwrapRef<typeof import('./composables/settings')['size']>
|
||||
readonly smallerScrollbars: UnwrapRef<typeof import('./composables/settings')['smallerScrollbars']>
|
||||
readonly softWrap: UnwrapRef<typeof import('./composables/settings')['softWrap']>
|
||||
readonly showAllContainers: UnwrapRef<typeof import('./stores/settings')['showAllContainers']>
|
||||
readonly showStd: UnwrapRef<typeof import('./stores/settings')['showStd']>
|
||||
readonly showTimestamp: UnwrapRef<typeof import('./stores/settings')['showTimestamp']>
|
||||
readonly size: UnwrapRef<typeof import('./stores/settings')['size']>
|
||||
readonly smallerScrollbars: UnwrapRef<typeof import('./stores/settings')['smallerScrollbars']>
|
||||
readonly softWrap: UnwrapRef<typeof import('./stores/settings')['softWrap']>
|
||||
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
|
||||
readonly stripVersion: UnwrapRef<typeof import('./utils/index')['stripVersion']>
|
||||
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 watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||
readonly withBase: UnwrapRef<typeof import('./stores/config')['withBase']>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createTestingPinia } from "@pinia/testing";
|
||||
import EventSource, { sources } from "eventsourcemock";
|
||||
import LogEventSource from "./LogEventSource.vue";
|
||||
import LogViewer from "./LogViewer.vue";
|
||||
import { settings } from "@/composables/settings";
|
||||
import { settings } from "@/stores/settings";
|
||||
import { useSearchFilter } from "@/composables/search";
|
||||
import { vi, describe, expect, beforeEach, test, afterEach } from "vitest";
|
||||
import { computed, nextTick } from "vue";
|
||||
@@ -14,6 +14,7 @@ import { containerContext } from "@/composables/containerContext";
|
||||
vi.mock("@/stores/config", () => ({
|
||||
__esModule: true,
|
||||
default: { base: "", hosts: [{ name: "localhost", id: "localhost" }] },
|
||||
withBase: (path: string) => path,
|
||||
}));
|
||||
|
||||
/**
|
||||
|
||||
@@ -85,7 +85,7 @@ export function useLogStream() {
|
||||
console.debug(`Connecting to ${containerId} with params`, params);
|
||||
|
||||
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", () => {
|
||||
close();
|
||||
@@ -118,7 +118,7 @@ export function useLogStream() {
|
||||
|
||||
const logs = await (
|
||||
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();
|
||||
if (logs) {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<script lang="ts" setup>
|
||||
// @ts-ignore - splitpanes types are not available
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
import { collapseNav } from "@/composables/settings";
|
||||
import { collapseNav } from "@/stores/settings";
|
||||
const { authorizationNeeded } = config;
|
||||
|
||||
const containerStore = useContainerStore();
|
||||
|
||||
@@ -2,13 +2,12 @@ import { type App } from "vue";
|
||||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import pages from "~pages";
|
||||
import { setupLayouts } from "virtual:generated-layouts";
|
||||
import config from "@/stores/config";
|
||||
|
||||
export const install = (app: App) => {
|
||||
const routes = setupLayouts(pages);
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(`${config.base}/`),
|
||||
history: createWebHistory(withBase("/")),
|
||||
routes,
|
||||
});
|
||||
|
||||
|
||||
@@ -49,14 +49,14 @@ let password = $ref("");
|
||||
let form: HTMLFormElement | undefined = $ref();
|
||||
|
||||
async function onLogin() {
|
||||
const response = await fetch(`${config.base}/api/validateCredentials`, {
|
||||
const response = await fetch(withBase("/api/validateCredentials"), {
|
||||
body: new FormData(form),
|
||||
method: "post",
|
||||
});
|
||||
|
||||
if (response.status == 200) {
|
||||
error = false;
|
||||
window.location.href = `${config.base}/`;
|
||||
window.location.href = withBase("/");
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ import {
|
||||
size,
|
||||
softWrap,
|
||||
automaticRedirect,
|
||||
} from "@/composables/settings";
|
||||
} from "@/stores/settings";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type Settings } from "@/stores/settings";
|
||||
|
||||
const text = document.querySelector("script#config__json")?.textContent || "{}";
|
||||
|
||||
interface Config {
|
||||
@@ -14,6 +16,7 @@ interface Config {
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
serverSettings?: Settings;
|
||||
}
|
||||
|
||||
const pageConfig = JSON.parse(text);
|
||||
@@ -25,4 +28,6 @@ const config: Config = {
|
||||
|
||||
config.version = config.version.replace(/^v/, "");
|
||||
|
||||
export default config;
|
||||
export default Object.freeze(config);
|
||||
|
||||
export const withBase = (path: string) => `${config.base}${path}`;
|
||||
|
||||
@@ -34,7 +34,7 @@ export const useContainerStore = defineStore("container", () => {
|
||||
function connect() {
|
||||
es?.close();
|
||||
ready.value = false;
|
||||
es = new EventSource(`${config.base}/api/events/stream`);
|
||||
es = new EventSource(withBase("/api/events/stream"));
|
||||
es.addEventListener("error", (e) => {
|
||||
if (es?.readyState === EventSource.CLOSED) {
|
||||
showToast(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toRefs } from "@vueuse/core";
|
||||
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
|
||||
|
||||
export const DEFAULT_SETTINGS: {
|
||||
export type Settings = {
|
||||
search: boolean;
|
||||
size: "small" | "medium" | "large";
|
||||
menuWidth: number;
|
||||
@@ -14,7 +14,8 @@ export const DEFAULT_SETTINGS: {
|
||||
softWrap: boolean;
|
||||
collapseNav: boolean;
|
||||
automaticRedirect: boolean;
|
||||
} = {
|
||||
};
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
search: true,
|
||||
size: "medium",
|
||||
menuWidth: 15,
|
||||
@@ -30,7 +31,14 @@ export const 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 {
|
||||
collapseNav,
|
||||
@@ -6,13 +6,11 @@ import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const RemoteUser contextKey = "remoteUser"
|
||||
const remoteUser contextKey = "remoteUser"
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
@@ -44,15 +42,21 @@ func newUser(username, email, name string) *User {
|
||||
|
||||
func ForwardProxyAuthorizationRequired(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Remote-Email") == "" {
|
||||
log.Error("Unable to find remote email. Please check your proxy configuration. Expecting header 'Remote-Email'")
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
if r.Header.Get("Remote-Email") != "" {
|
||||
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))
|
||||
} 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
|
||||
}
|
||||
|
||||
99
internal/profile/settings.go
Normal file
99
internal/profile/settings.go
Normal 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
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/amir20/dozzle/internal/analytics"
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/amir20/dozzle/internal/docker"
|
||||
"github.com/amir20/dozzle/internal/profile"
|
||||
|
||||
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)
|
||||
if err == nil && req.URL.Path != "" && req.URL.Path != "/" {
|
||||
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 {
|
||||
if !isAuthorized(req) && req.URL.Path != "login" {
|
||||
http.Redirect(w, req, path.Clean(h.config.Base+"/login"), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
go h.sendRequestEvent()
|
||||
h.executeTemplate(w, req)
|
||||
}
|
||||
}
|
||||
@@ -99,9 +73,23 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
"hosts": hosts,
|
||||
}
|
||||
|
||||
if h.config.AuthProvider == "forward-proxy" {
|
||||
user := req.Context().Value(auth.RemoteUser).(*auth.User)
|
||||
if h.config.AuthProvider == FORWARD_PROXY {
|
||||
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
|
||||
if settings, err := profile.LoadUserSettings(user); err == nil {
|
||||
config["serverSettings"] = settings
|
||||
} else {
|
||||
config["serverSettings"] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
@@ -138,5 +126,33 @@ func (h *handler) readManifest() map[string]interface{} {
|
||||
}
|
||||
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
32
internal/web/profile.go
Normal 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)
|
||||
}
|
||||
@@ -16,6 +16,13 @@ import (
|
||||
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
|
||||
type Config struct {
|
||||
Base string
|
||||
@@ -26,7 +33,7 @@ type Config struct {
|
||||
Hostname string
|
||||
NoAnalytics bool
|
||||
Dev bool
|
||||
AuthProvider string
|
||||
AuthProvider AuthProvider
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
@@ -70,7 +77,7 @@ func createRouter(h *handler) *chi.Mux {
|
||||
|
||||
r.Route(base, 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.Group(func(r chi.Router) {
|
||||
@@ -81,6 +88,7 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Get("/api/events/stream", h.streamEvents)
|
||||
r.Get("/logout", h.clearSession)
|
||||
r.Get("/version", h.version)
|
||||
r.Put("/api/profile/settings", h.saveSettings)
|
||||
})
|
||||
|
||||
defaultHandler := http.StripPrefix(strings.Replace(base+"/", "//", "/", 1), http.HandlerFunc(h.index))
|
||||
|
||||
10
main.go
10
main.go
@@ -172,6 +172,14 @@ func createClients(args args,
|
||||
|
||||
func createServer(args args, clients map[string]web.DockerClient) *http.Server {
|
||||
_, 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{
|
||||
Addr: args.Addr,
|
||||
Base: args.Base,
|
||||
@@ -181,7 +189,7 @@ func createServer(args args, clients map[string]web.DockerClient) *http.Server {
|
||||
Hostname: args.Hostname,
|
||||
NoAnalytics: args.NoAnalytics,
|
||||
Dev: dev,
|
||||
AuthProvider: args.AuthProvider,
|
||||
AuthProvider: provider,
|
||||
}
|
||||
|
||||
assets, err := fs.Sub(content, "dist")
|
||||
|
||||
0
public/favicon.ico
Normal file
0
public/favicon.ico
Normal file
@@ -65,6 +65,9 @@ export default defineConfig(() => ({
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ["**/data/**"],
|
||||
},
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: {
|
||||
|
||||
Reference in New Issue
Block a user