mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-25 23:03:47 +01:00
feat: syncs all profile localStorage to disk (#2537)
This commit is contained in:
3
assets/auto-imports.d.ts
vendored
3
assets/auto-imports.d.ts
vendored
@@ -268,6 +268,7 @@ declare global {
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||
const useProfileStorage: typeof import('./composable/profileStorage')['useProfileStorage']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useReleases: typeof import('./stores/releases')['useReleases']
|
||||
@@ -617,6 +618,7 @@ declare module 'vue' {
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||
readonly useProfileStorage: UnwrapRef<typeof import('./composable/profileStorage')['useProfileStorage']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
|
||||
@@ -959,6 +961,7 @@ declare module '@vue/runtime-core' {
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||
readonly useProfileStorage: UnwrapRef<typeof import('./composable/profileStorage')['useProfileStorage']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useReleases: UnwrapRef<typeof import('./stores/releases')['useReleases']>
|
||||
|
||||
@@ -59,5 +59,5 @@ async function logout() {
|
||||
}
|
||||
|
||||
const { hasUpdate, latest } = useReleases();
|
||||
const latestTag = useStorage("DOZZLE_LATEST_TAG", config.version);
|
||||
const latestTag = useProfileStorage("releaseSeen", config.version);
|
||||
</script>
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
<script lang="ts" setup>
|
||||
const { container } = useContainerContext();
|
||||
const pinned = computed({
|
||||
get: () => pinnedContainers.value.has(container.value.storageKey),
|
||||
get: () => pinnedContainers.value.has(container.value.name),
|
||||
set: (value) => {
|
||||
if (value) {
|
||||
pinnedContainers.value.add(container.value.storageKey);
|
||||
pinnedContainers.value.add(container.value.name);
|
||||
} else {
|
||||
pinnedContainers.value.delete(container.value.storageKey);
|
||||
pinnedContainers.value.delete(container.value.name);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ const sortedContainers = computed(() =>
|
||||
const groupedContainers = computed(() =>
|
||||
sortedContainers.value.reduce(
|
||||
(acc, item) => {
|
||||
if (debouncedIds.value.has(item.storageKey)) {
|
||||
if (debouncedIds.value.has(item.name)) {
|
||||
acc.pinned.push(item);
|
||||
} else {
|
||||
acc.unpinned.push(item);
|
||||
|
||||
36
assets/composable/profileStorage.ts
Normal file
36
assets/composable/profileStorage.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Profile } from "@/stores/config";
|
||||
|
||||
export function useProfileStorage<K extends keyof Profile>(key: K, defaultValue: NonNullable<Profile[K]>) {
|
||||
const storageKey = "DOZZLE_" + key.toUpperCase();
|
||||
const storage = useStorage<NonNullable<Profile[K]>>(storageKey, defaultValue, undefined, {
|
||||
writeDefaults: false,
|
||||
mergeDefaults: true,
|
||||
});
|
||||
|
||||
if (config.profile?.[key]) {
|
||||
if (storage.value instanceof Set && config.profile[key] instanceof Array) {
|
||||
storage.value = new Set([...(config.profile[key] as Iterable<any>)]) as unknown as NonNullable<Profile[K]>;
|
||||
} else if (config.profile[key] instanceof Array) {
|
||||
storage.value = config.profile[key] as NonNullable<Profile[K]>;
|
||||
} else if (config.profile[key] instanceof Object) {
|
||||
Object.assign(storage.value, config.profile[key]);
|
||||
} else {
|
||||
storage.value = config.profile[key] as NonNullable<Profile[K]>;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.user) {
|
||||
watch(
|
||||
storage,
|
||||
(value) => {
|
||||
fetch(withBase("/api/profile"), {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ [key]: value }, (_, value) => (value instanceof Set ? [...value] : value)),
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
}
|
||||
|
||||
return storage;
|
||||
}
|
||||
@@ -7,16 +7,18 @@ if (config.hosts.length === 1 && !sessionHost.value) {
|
||||
sessionHost.value = config.hosts[0].id;
|
||||
}
|
||||
|
||||
export function persistentVisibleKeys(container: Ref<Container>) {
|
||||
const storage = useStorage<{ [key: string]: string[][] }>("DOZZLE_VISIBLE_KEYS", {});
|
||||
export function persistentVisibleKeys(container: Ref<Container>): Ref<string[][]> {
|
||||
const storage = useProfileStorage("visibleKeys", {});
|
||||
return computed(() => {
|
||||
if (!(container.value.storageKey in storage.value)) {
|
||||
storage.value[container.value.storageKey] = [];
|
||||
// Returning a temporary ref here to avoid writing an empty array to storage
|
||||
const visibleKeys = ref<string[][]>([]);
|
||||
watchOnce(visibleKeys, () => (storage.value[container.value.storageKey] = visibleKeys.value), { deep: true });
|
||||
return visibleKeys.value;
|
||||
}
|
||||
|
||||
return storage.value[container.value.storageKey];
|
||||
});
|
||||
}
|
||||
|
||||
const DOZZLE_PINNED_CONTAINERS = "DOZZLE_PINNED_CONTAINERS";
|
||||
export const pinnedContainers = useStorage(DOZZLE_PINNED_CONTAINERS, new Set<string>());
|
||||
export const pinnedContainers = useProfileStorage("pinned", new Set<string>());
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type Settings } from "@/stores/settings";
|
||||
|
||||
const text = document.querySelector("script#config__json")?.textContent || "{}";
|
||||
|
||||
interface Config {
|
||||
export interface Config {
|
||||
version: string;
|
||||
base: string;
|
||||
authorizationNeeded: boolean;
|
||||
@@ -17,10 +17,17 @@ interface Config {
|
||||
name: string;
|
||||
avatar: string;
|
||||
};
|
||||
serverSettings?: Settings;
|
||||
profile?: Profile;
|
||||
pages?: { id: string; title: string }[];
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
settings?: Settings;
|
||||
pinned?: Set<string>;
|
||||
visibleKeys?: { [key: string]: string[][] };
|
||||
releaseSeen?: string;
|
||||
}
|
||||
|
||||
const pageConfig = JSON.parse(text);
|
||||
|
||||
const config: Config = {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { toRefs } from "@vueuse/core";
|
||||
const DOZZLE_SETTINGS_KEY = "DOZZLE_SETTINGS";
|
||||
|
||||
export type Settings = {
|
||||
search: boolean;
|
||||
@@ -30,17 +29,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
automaticRedirect: true,
|
||||
};
|
||||
|
||||
export const settings = useStorage(DOZZLE_SETTINGS_KEY, DEFAULT_SETTINGS);
|
||||
settings.value = { ...DEFAULT_SETTINGS, ...settings.value, ...config.serverSettings };
|
||||
|
||||
if (config.user) {
|
||||
watch(settings, (value) => {
|
||||
fetch(withBase("/api/profile/settings"), {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(value),
|
||||
});
|
||||
});
|
||||
}
|
||||
export const settings = useProfileStorage("settings", DEFAULT_SETTINGS);
|
||||
|
||||
export const {
|
||||
collapseNav,
|
||||
|
||||
@@ -3,6 +3,8 @@ package profile
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -11,6 +13,12 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
profileFilename = "profile.json"
|
||||
)
|
||||
|
||||
var missingProfileErr = errors.New("Profile file does not exist")
|
||||
|
||||
type Settings struct {
|
||||
Search bool `json:"search"`
|
||||
MenuWidth float32 `json:"menuWidth"`
|
||||
@@ -26,7 +34,15 @@ type Settings struct {
|
||||
HourStyle string `json:"hourStyle,omitempty"`
|
||||
}
|
||||
|
||||
var data_path string
|
||||
type Profile struct {
|
||||
Settings *Settings `json:"settings,omitempty"`
|
||||
Pinned []string `json:"pinned,omitempty"`
|
||||
VisibleKeys map[string][][]string `json:"visibleKeys,omitempty"`
|
||||
ReleaseSeen string `json:"releaseSeen,omitempty"`
|
||||
}
|
||||
|
||||
var dataPath string
|
||||
var mux = &sync.Mutex{}
|
||||
|
||||
func init() {
|
||||
path, err := filepath.Abs("./data")
|
||||
@@ -40,28 +56,40 @@ func init() {
|
||||
return
|
||||
}
|
||||
}
|
||||
data_path = path
|
||||
dataPath = path
|
||||
}
|
||||
|
||||
func SaveUserSettings(user auth.User, settings Settings) error {
|
||||
path := filepath.Join(data_path, user.Username)
|
||||
func UpdateFromReader(user auth.User, reader io.Reader) error {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
existingProfile, err := Load(user)
|
||||
if err != nil && err != missingProfileErr {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create user directory if it doesn't exist
|
||||
if err := json.NewDecoder(reader).Decode(&existingProfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Save(user, existingProfile)
|
||||
}
|
||||
|
||||
func Save(user auth.User, profile Profile) error {
|
||||
path := filepath.Join(dataPath, user.Username)
|
||||
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, "", " ")
|
||||
filePath := filepath.Join(path, profileFilename)
|
||||
data, err := json.MarshalIndent(profile, "", " ")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(settings_path)
|
||||
f, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -76,24 +104,24 @@ func SaveUserSettings(user auth.User, settings Settings) error {
|
||||
return f.Sync()
|
||||
}
|
||||
|
||||
func LoadUserSettings(user auth.User) (Settings, error) {
|
||||
path := filepath.Join(data_path, user.Username)
|
||||
settings_path := filepath.Join(path, "settings.json")
|
||||
func Load(user auth.User) (Profile, error) {
|
||||
path := filepath.Join(dataPath, user.Username)
|
||||
profilePath := filepath.Join(path, profileFilename)
|
||||
|
||||
if _, err := os.Stat(settings_path); os.IsNotExist(err) {
|
||||
return Settings{}, errors.New("Settings file does not exist")
|
||||
if _, err := os.Stat(profilePath); os.IsNotExist(err) {
|
||||
return Profile{}, missingProfileErr
|
||||
}
|
||||
|
||||
f, err := os.Open(settings_path)
|
||||
f, err := os.Open(profilePath)
|
||||
if err != nil {
|
||||
return Settings{}, err
|
||||
return Profile{}, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var settings Settings
|
||||
if err := json.NewDecoder(f).Decode(&settings); err != nil {
|
||||
return Settings{}, err
|
||||
var profile Profile
|
||||
if err := json.NewDecoder(f).Decode(&profile); err != nil {
|
||||
return Profile{}, err
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
return profile, nil
|
||||
}
|
||||
@@ -65,10 +65,10 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
user := auth.UserFromContext(req.Context())
|
||||
if user != nil {
|
||||
if settings, err := profile.LoadUserSettings(*user); err == nil {
|
||||
config["serverSettings"] = settings
|
||||
if profile, err := profile.Load(*user); err == nil {
|
||||
config["profile"] = profile
|
||||
} else {
|
||||
config["serverSettings"] = struct{}{}
|
||||
config["profile"] = struct{}{}
|
||||
}
|
||||
config["user"] = user
|
||||
} else if h.config.Authorization.Provider == FORWARD_PROXY {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
@@ -9,20 +8,14 @@ import (
|
||||
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
|
||||
}
|
||||
|
||||
func (h *handler) updateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.UserFromContext(r.Context())
|
||||
if user == nil {
|
||||
http.Error(w, "Unable to find user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := profile.SaveUserSettings(*user, settings); err != nil {
|
||||
if err := profile.UpdateFromReader(*user, r.Body); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Errorf("Unable to save user settings: %s", err)
|
||||
return
|
||||
|
||||
@@ -105,7 +105,7 @@ func createRouter(h *handler) *chi.Mux {
|
||||
r.Get("/api/logs/{host}/{id}", h.fetchLogsBetweenDates)
|
||||
r.Get("/api/events/stream", h.streamEvents)
|
||||
r.Get("/api/releases", h.releases)
|
||||
r.Put("/api/profile/settings", h.saveSettings)
|
||||
r.Patch("/api/profile", h.updateProfile)
|
||||
r.Get("/api/content/{id}", h.staticContent)
|
||||
r.Get("/logout", h.clearSession) // TODO remove this
|
||||
r.Get("/version", h.version)
|
||||
|
||||
Reference in New Issue
Block a user