From 1e37c33603456b428f1638e0d6b9e02b3edc0a02 Mon Sep 17 00:00:00 2001 From: cclohmar Date: Fri, 10 Jul 2026 12:25:18 +0100 Subject: [PATCH] feat: user settings page with profile, mail, preferences --- main.go | 196 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 171 insertions(+), 25 deletions(-) diff --git a/main.go b/main.go index 7e10010..cd4f0da 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/json" "fmt" "html/template" "io" @@ -70,6 +71,36 @@ type Settings struct { } `yaml:"imap"` } +// --- User settings --- + +type UserSettings struct { + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + ProfilePicture string `json:"profile_picture,omitempty"` + Email string `json:"email,omitempty"` + EmailPassword string `json:"email_password,omitempty"` + Language string `json:"language,omitempty"` + Timezone string `json:"timezone,omitempty"` +} + +func loadUserSettings(username string) *UserSettings { + path := filepath.Join("/opt/nextworkspace/data/users", username+".json") + data, err := os.ReadFile(path) + if err != nil { + return &UserSettings{} + } + var s UserSettings + json.Unmarshal(data, &s) + return &s +} + +func saveUserSettings(username string, s *UserSettings) error { + path := filepath.Join("/opt/nextworkspace/data/users", username+".json") + os.MkdirAll(filepath.Dir(path), 0700) + data, _ := json.MarshalIndent(s, "", " ") + return os.WriteFile(path, data, 0600) +} + // --- Config loading --- func loadConfig(configDir string) (*Config, error) { @@ -201,7 +232,22 @@ func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc { groupsHeader := r.Header.Get("Remote-Groups") userGroups := strings.Split(groupsHeader, ",") settings, _ := loadSettings(os.Getenv("CONFIG_DIR")) - lang := getUserLang(r) + us := loadUserSettings(user) + lang := us.Language + if lang == "" && settings != nil { + lang = settings.Company.Language + } + if lang == "" { + lang = "en" + } + // Use display name if available + displayName := user + if us.FirstName != "" { + displayName = us.FirstName + if us.LastName != "" { + displayName += " " + us.LastName + } + } var allowedApps []AppEntry for _, app := range apps { @@ -214,6 +260,7 @@ func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc { AppName string Description string User string + DisplayName string IsAdmin bool Apps []AppEntry CompanyLogo string @@ -223,6 +270,7 @@ func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc { AppName: cfg.App.Name, Description: cfg.App.Description, User: user, + DisplayName: displayName, IsAdmin: strings.Contains(groupsHeader, "admins"), Apps: allowedApps, CompanyLogo: settings.Company.Logo, @@ -358,15 +406,53 @@ func settingsHandler(w http.ResponseWriter, r *http.Request) { user := r.Header.Get("Remote-User") groups := r.Header.Get("Remote-Groups") isAdmin := strings.Contains(groups, "admins") + us := loadUserSettings(user) + settings, _ := loadSettings(os.Getenv("CONFIG_DIR")) + lang := us.Language + if lang == "" && settings != nil { + lang = settings.Company.Language + } + if lang == "" { + lang = "en" + } tmpl := template.Must(template.New("settings").Parse(settingsHTML)) w.Header().Set("Content-Type", "text/html; charset=utf-8") tmpl.Execute(w, map[string]interface{}{ - "User": user, - "IsAdmin": isAdmin, + "User": user, + "IsAdmin": isAdmin, + "UserSettings": us, + "Settings": settings, + "Lang": lang, }) } +func userSettingsSaveHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + user := r.Header.Get("Remote-User") + if user == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + us := &UserSettings{ + FirstName: r.FormValue("first_name"), + LastName: r.FormValue("last_name"), + ProfilePicture: r.FormValue("profile_picture"), + Email: r.FormValue("email"), + EmailPassword: r.FormValue("email_password"), + Language: r.FormValue("language"), + Timezone: r.FormValue("timezone"), + } + if err := saveUserSettings(user, us); err != nil { + http.Error(w, "Failed to save", http.StatusInternalServerError) + return + } + w.Write([]byte(`{"status":"ok"}`)) +} + // --- API proxy for authelia-api --- func apiProxyHandler(w http.ResponseWriter, r *http.Request) { @@ -641,7 +727,7 @@ const launcherHTML = ` {{if .Subtitle}}

{{.Subtitle}}

{{end}}
- {{t .Lang "welcome" "user" .User}} + {{t .Lang "welcome" "user" .DisplayName}} {{t .Lang "settings"}} {{t .Lang "logout"}}
@@ -662,44 +748,104 @@ const launcherHTML = ` ` const settingsHTML = ` - + - Settings — NextWorkspace + {{t .Lang "settings"}} — NextWorkspace
-

Settings

- Back to Launcher +

{{t .Lang "settings"}}

+ {{t .Lang "launcher_title"}}
-
-

Account

-

Logged in as {{.User}}

-
+
+
+

Profile

+
+
+
+
+
+
+
+

Mail

+

Server: {{.Settings.IMAP.Host}}:{{.Settings.IMAP.Port}}

+
+
+
+
+

Preferences

+
+
+ + +
+
+ + +
+
+
+
+ + {{t .Lang "saved"}} +
+
{{if .IsAdmin}}

Administration

-

Manage users, groups, and workspace configuration.

- ⚖ Admin Panel → +

Manage users, groups, and workspace configuration.

+ ⚖ Admin Panel →
{{end}}
+ ` @@ -1188,7 +1334,7 @@ func main() { // Protected: settings mux.Handle("/settings", authMiddleware(settingsHandler)) - mux.Handle("/settings/", authMiddleware(settingsHandler)) + mux.Handle("/settings/save", authMiddleware(http.HandlerFunc(userSettingsSaveHandler))) // Protected: admin — admins only mux.Handle("/config", adminGroupMiddleware(authMiddleware(adminHandler)))