mirror of
https://github.com/amir20/dozzle.git
synced 2026-06-23 04:10:12 +00:00
feat: always persist user profile to disk (#4550)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ export function useProfileStorage<K extends keyof Profile>(
|
||||
}
|
||||
}
|
||||
|
||||
if (config.user) {
|
||||
if (config.user || config.authProvider === "none") {
|
||||
watch(
|
||||
storage,
|
||||
(value) => {
|
||||
@@ -56,6 +56,8 @@ export function useProfileStorage<K extends keyof Profile>(
|
||||
return value;
|
||||
}
|
||||
}),
|
||||
}).catch((e) => {
|
||||
console.error(`Failed to sync ${key} to profile`, e);
|
||||
});
|
||||
},
|
||||
{ deep: true },
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/amir20/dozzle/internal/auth"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
profileFilename = "profile.json"
|
||||
profileFilename = "profile.json"
|
||||
DefaultUsername = "__default__"
|
||||
)
|
||||
|
||||
var errMissingProfileErr = errors.New("Profile file does not exist")
|
||||
@@ -48,6 +48,7 @@ type Profile struct {
|
||||
|
||||
var dataPath string
|
||||
var mux = &sync.Mutex{}
|
||||
var errInvalidUsername = errors.New("invalid username: contains path separator or traversal")
|
||||
|
||||
func init() {
|
||||
path, err := filepath.Abs("./data")
|
||||
@@ -64,10 +65,18 @@ func init() {
|
||||
dataPath = path
|
||||
}
|
||||
|
||||
func UpdateFromReader(user auth.User, reader io.Reader) error {
|
||||
func safePath(username string) (string, error) {
|
||||
clean := filepath.Base(username)
|
||||
if clean != username || clean == "." || clean == ".." {
|
||||
return "", errInvalidUsername
|
||||
}
|
||||
return filepath.Join(dataPath, clean), nil
|
||||
}
|
||||
|
||||
func UpdateFromReader(username string, reader io.Reader) error {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
existingProfile, err := Load(user)
|
||||
existingProfile, err := Load(username)
|
||||
if err != nil && err != errMissingProfileErr {
|
||||
log.Error().Err(err).Msg("Unable to load profile. Overwriting it.")
|
||||
}
|
||||
@@ -76,11 +85,14 @@ func UpdateFromReader(user auth.User, reader io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return save(user, existingProfile)
|
||||
return save(username, existingProfile)
|
||||
}
|
||||
|
||||
func save(user auth.User, profile Profile) error {
|
||||
path := filepath.Join(dataPath, user.Username)
|
||||
func save(username string, profile Profile) error {
|
||||
path, err := safePath(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if err := os.Mkdir(path, 0755); err != nil {
|
||||
return err
|
||||
@@ -109,8 +121,11 @@ func save(user auth.User, profile Profile) error {
|
||||
return f.Sync()
|
||||
}
|
||||
|
||||
func Load(user auth.User) (Profile, error) {
|
||||
path := filepath.Join(dataPath, user.Username)
|
||||
func Load(username string) (Profile, error) {
|
||||
path, err := safePath(username)
|
||||
if err != nil {
|
||||
return Profile{}, err
|
||||
}
|
||||
profilePath := filepath.Join(path, profileFilename)
|
||||
|
||||
if _, err := os.Stat(profilePath); os.IsNotExist(err) {
|
||||
|
||||
+48
-37
@@ -33,31 +33,38 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
base = h.config.Base
|
||||
}
|
||||
|
||||
hosts := h.hostService.Hosts()
|
||||
sort.Slice(hosts, func(i, j int) bool {
|
||||
return hosts[i].Name < hosts[j].Name
|
||||
})
|
||||
user := auth.UserFromContext(req.Context())
|
||||
|
||||
// Handle unauthorized cases early
|
||||
if user == nil {
|
||||
switch h.config.Authorization.Provider {
|
||||
case FORWARD_PROXY:
|
||||
log.Error().Msg("Unable to find remote user. Please check your proxy configuration. Expecting headers Remote-Email, Remote-User, Remote-Name.")
|
||||
log.Debug().Str("url", req.URL.String()).Msg("Dumping all headers for request")
|
||||
for k, v := range req.Header {
|
||||
log.Debug().Strs(k, v).Send()
|
||||
}
|
||||
http.Error(w, "Unauthorized user", http.StatusUnauthorized)
|
||||
return
|
||||
case SIMPLE:
|
||||
if req.URL.Path != "login" {
|
||||
log.Debug().Str("url", req.URL.String()).Msg("Redirecting to login page")
|
||||
http.Redirect(w, req, path.Clean(h.config.Base+"/login")+"?redirectUrl=/"+req.URL.String(), http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config := map[string]interface{}{
|
||||
"base": base,
|
||||
}
|
||||
|
||||
user := auth.UserFromContext(req.Context())
|
||||
|
||||
// Build full config when authorized (no auth or authenticated user)
|
||||
if h.config.Authorization.Provider == NONE || user != nil {
|
||||
if user != nil {
|
||||
config["enableShell"] = h.config.EnableShell && user.Roles.Has(auth.Shell)
|
||||
config["enableActions"] = h.config.EnableActions && user.Roles.Has(auth.Actions)
|
||||
config["enableDownload"] = user.Roles.Has(auth.Download)
|
||||
} else {
|
||||
config["enableShell"] = h.config.EnableShell
|
||||
config["enableActions"] = h.config.EnableActions
|
||||
config["enableDownload"] = true
|
||||
}
|
||||
|
||||
if h.config.Authorization.Provider == FORWARD_PROXY && strings.TrimSpace(h.config.Authorization.LogoutUrl) != "" {
|
||||
config["logoutUrl"] = strings.TrimSpace(h.config.Authorization.LogoutUrl)
|
||||
}
|
||||
hosts := h.hostService.Hosts()
|
||||
sort.Slice(hosts, func(i, j int) bool {
|
||||
return hosts[i].Name < hosts[j].Name
|
||||
})
|
||||
|
||||
config["authProvider"] = h.config.Authorization.Provider
|
||||
config["version"] = h.config.Version
|
||||
@@ -66,27 +73,31 @@ func (h *handler) executeTemplate(w http.ResponseWriter, req *http.Request) {
|
||||
config["hosts"] = hosts
|
||||
config["disableAvatars"] = h.config.DisableAvatars
|
||||
config["releaseCheckMode"] = h.config.ReleaseCheckMode
|
||||
config["enableShell"] = h.config.EnableShell
|
||||
config["enableActions"] = h.config.EnableActions
|
||||
config["enableDownload"] = true
|
||||
|
||||
if user != nil {
|
||||
config["enableShell"] = h.config.EnableShell && user.Roles.Has(auth.Shell)
|
||||
config["enableActions"] = h.config.EnableActions && user.Roles.Has(auth.Actions)
|
||||
config["enableDownload"] = user.Roles.Has(auth.Download)
|
||||
config["user"] = user
|
||||
}
|
||||
|
||||
if h.config.Authorization.Provider == FORWARD_PROXY && strings.TrimSpace(h.config.Authorization.LogoutUrl) != "" {
|
||||
config["logoutUrl"] = strings.TrimSpace(h.config.Authorization.LogoutUrl)
|
||||
}
|
||||
}
|
||||
|
||||
profileUsername := profile.DefaultUsername
|
||||
if user != nil {
|
||||
if profile, err := profile.Load(*user); err == nil {
|
||||
config["profile"] = profile
|
||||
} else {
|
||||
config["profile"] = struct{}{}
|
||||
}
|
||||
config["user"] = user
|
||||
} else if h.config.Authorization.Provider == FORWARD_PROXY {
|
||||
log.Error().Msg("Unable to find remote user. Please check your proxy configuration. Expecting headers Remote-Email, Remote-User, Remote-Name.")
|
||||
log.Debug().Str("url", req.URL.String()).Msg("Dumping all headers for request")
|
||||
for k, v := range req.Header {
|
||||
log.Debug().Strs(k, v).Send()
|
||||
}
|
||||
http.Error(w, "Unauthorized user", http.StatusUnauthorized)
|
||||
return
|
||||
} else if h.config.Authorization.Provider == SIMPLE && req.URL.Path != "login" {
|
||||
log.Debug().Str("url", req.URL.String()).Msg("Redirecting to login page")
|
||||
http.Redirect(w, req, path.Clean(h.config.Base+"/login")+"?redirectUrl=/"+req.URL.String(), http.StatusTemporaryRedirect)
|
||||
return
|
||||
profileUsername = user.Username
|
||||
}
|
||||
|
||||
if loadedProfile, err := profile.Load(profileUsername); err == nil {
|
||||
config["profile"] = loadedProfile
|
||||
} else {
|
||||
config["profile"] = struct{}{}
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
|
||||
@@ -10,13 +10,12 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
username := profile.DefaultUsername
|
||||
if user := auth.UserFromContext(r.Context()); user != nil {
|
||||
username = user.Username
|
||||
}
|
||||
|
||||
if err := profile.UpdateFromReader(*user, r.Body); err != nil {
|
||||
if err := profile.UpdateFromReader(username, r.Body); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Error().Err(err).Msg("Failed to update profile")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user