From 3926954b747b2a75bdf110e2a0d6c765f942b9ad Mon Sep 17 00:00:00 2001 From: LordMathis Date: Tue, 28 Oct 2025 20:05:12 +0100 Subject: [PATCH] Add theme to user preferences --- .gitignore | 3 +++ server/internal/app/init.go | 4 ++- .../postgres/001_initial_schema.up.sql | 3 ++- .../sqlite/001_initial_schema.up.sql | 3 ++- server/internal/handlers/admin_handlers.go | 27 +++++++++++++++++++ server/internal/handlers/user_handlers.go | 14 ++++++++++ .../internal/handlers/workspace_handlers.go | 17 +++++++++++- server/internal/models/user.go | 1 + server/internal/models/workspace.go | 4 +-- 9 files changed, 70 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 1c632f7..d17cc3b 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,6 @@ go.work.sum main *.db data + +# Feature specifications +spec.md \ No newline at end of file diff --git a/server/internal/app/init.go b/server/internal/app/init.go index 1d0bc08..263459e 100644 --- a/server/internal/app/init.go +++ b/server/internal/app/init.go @@ -118,6 +118,7 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C DisplayName: "Admin", PasswordHash: string(hashedPassword), Role: models.RoleAdmin, + Theme: "dark", // default theme } createdUser, err := database.CreateUser(adminUser) @@ -132,7 +133,8 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C logging.Info("admin user setup completed", "userId", createdUser.ID, - "workspaceId", createdUser.LastWorkspaceID) + "workspaceId", createdUser.LastWorkspaceID, + "theme", createdUser.Theme) return nil } diff --git a/server/internal/db/migrations/postgres/001_initial_schema.up.sql b/server/internal/db/migrations/postgres/001_initial_schema.up.sql index 3d08d60..f0c8e63 100644 --- a/server/internal/db/migrations/postgres/001_initial_schema.up.sql +++ b/server/internal/db/migrations/postgres/001_initial_schema.up.sql @@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS users ( display_name TEXT, password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')), + theme TEXT NOT NULL DEFAULT 'dark' CHECK(theme IN ('light', 'dark')), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_workspace_id INTEGER ); @@ -19,7 +20,7 @@ CREATE TABLE IF NOT EXISTS workspaces ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_opened_file_path TEXT, -- Settings fields - theme TEXT NOT NULL DEFAULT 'light' CHECK(theme IN ('light', 'dark')), + theme TEXT NOT NULL DEFAULT 'dark' CHECK(theme IN ('light', 'dark')), auto_save BOOLEAN NOT NULL DEFAULT FALSE, git_enabled BOOLEAN NOT NULL DEFAULT FALSE, git_url TEXT, diff --git a/server/internal/db/migrations/sqlite/001_initial_schema.up.sql b/server/internal/db/migrations/sqlite/001_initial_schema.up.sql index a718161..d33757b 100644 --- a/server/internal/db/migrations/sqlite/001_initial_schema.up.sql +++ b/server/internal/db/migrations/sqlite/001_initial_schema.up.sql @@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS users ( display_name TEXT, password_hash TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')), + theme TEXT NOT NULL DEFAULT 'dark' CHECK(theme IN ('light', 'dark')), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_workspace_id INTEGER ); @@ -18,7 +19,7 @@ CREATE TABLE IF NOT EXISTS workspaces ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_opened_file_path TEXT, -- Settings fields - theme TEXT NOT NULL DEFAULT 'light' CHECK(theme IN ('light', 'dark')), + theme TEXT NOT NULL DEFAULT 'dark' CHECK(theme IN ('light', 'dark')), auto_save BOOLEAN NOT NULL DEFAULT 0, git_enabled BOOLEAN NOT NULL DEFAULT 0, git_url TEXT, diff --git a/server/internal/handlers/admin_handlers.go b/server/internal/handlers/admin_handlers.go index 47c9d04..4a727ec 100644 --- a/server/internal/handlers/admin_handlers.go +++ b/server/internal/handlers/admin_handlers.go @@ -22,6 +22,7 @@ type CreateUserRequest struct { DisplayName string `json:"displayName"` Password string `json:"password"` Role models.UserRole `json:"role"` + Theme string `json:"theme,omitempty"` } // UpdateUserRequest holds the request fields for updating a user @@ -30,6 +31,7 @@ type UpdateUserRequest struct { DisplayName string `json:"displayName,omitempty"` Password string `json:"password,omitempty"` Role models.UserRole `json:"role,omitempty"` + Theme string `json:"theme,omitempty"` } // WorkspaceStats holds workspace statistics @@ -164,11 +166,24 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc { return } + // Handle theme with validation and default + theme := req.Theme + if theme == "" { + theme = "dark" // Default theme + } else if theme != "light" && theme != "dark" { + // Invalid theme, fallback to dark + log.Debug("invalid theme value in user creation, falling back to dark", + "theme", theme, + ) + theme = "dark" + } + user := &models.User{ Email: req.Email, DisplayName: req.DisplayName, PasswordHash: string(hashedPassword), Role: req.Role, + Theme: theme, } insertedUser, err := h.DB.CreateUser(user) @@ -196,6 +211,7 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc { "newUserID", insertedUser.ID, "email", insertedUser.Email, "role", insertedUser.Role, + "theme", insertedUser.Theme, ) respondJSON(w, insertedUser) } @@ -322,6 +338,17 @@ func (h *Handler) AdminUpdateUser() http.HandlerFunc { user.Role = req.Role updates["role"] = req.Role } + if req.Theme != "" { + // Validate theme value, fallback to "dark" if invalid + if req.Theme != "light" && req.Theme != "dark" { + log.Debug("invalid theme value, falling back to dark", + "theme", req.Theme, + ) + req.Theme = "dark" + } + user.Theme = req.Theme + updates["theme"] = req.Theme + } if req.Password != "" { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { diff --git a/server/internal/handlers/user_handlers.go b/server/internal/handlers/user_handlers.go index 9984d80..6db0a1c 100644 --- a/server/internal/handlers/user_handlers.go +++ b/server/internal/handlers/user_handlers.go @@ -16,6 +16,7 @@ type UpdateProfileRequest struct { Email string `json:"email"` CurrentPassword string `json:"currentPassword"` NewPassword string `json:"newPassword"` + Theme string `json:"theme"` } // DeleteAccountRequest represents a user account deletion request @@ -149,6 +150,19 @@ func (h *Handler) UpdateProfile() http.HandlerFunc { updates["displayNameChanged"] = true } + // Update theme if provided + if req.Theme != "" { + // Validate theme value, fallback to "dark" if invalid + if req.Theme != "light" && req.Theme != "dark" { + log.Debug("invalid theme value, falling back to dark", + "theme", req.Theme, + ) + req.Theme = "dark" + } + user.Theme = req.Theme + updates["themeChanged"] = true + } + // Update user in database if err := h.DB.UpdateUser(user); err != nil { log.Error("failed to update user in database", diff --git a/server/internal/handlers/workspace_handlers.go b/server/internal/handlers/workspace_handlers.go index 788b76f..6643f4d 100644 --- a/server/internal/handlers/workspace_handlers.go +++ b/server/internal/handlers/workspace_handlers.go @@ -87,7 +87,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { "clientIP", r.RemoteAddr, ) - var workspace models.Workspace + var workspace models.Workspace if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil { log.Debug("invalid request body received", "error", err.Error(), @@ -104,7 +104,21 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { return } + // Get user to access their theme preference + user, err := h.DB.GetUserByID(ctx.UserID) + if err != nil { + log.Error("failed to fetch user from database", + "error", err.Error(), + ) + respondError(w, "Failed to get user", http.StatusInternalServerError) + return + } + workspace.UserID = ctx.UserID + // Use user's theme as default if not provided + if workspace.Theme == "" { + workspace.Theme = user.Theme + } if err := h.DB.CreateWorkspace(&workspace); err != nil { log.Error("failed to create workspace in database", "error", err.Error(), @@ -145,6 +159,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { log.Info("workspace created", "workspaceID", workspace.ID, "workspaceName", workspace.Name, + "theme", workspace.Theme, "gitEnabled", workspace.GitEnabled, ) respondJSON(w, workspace) diff --git a/server/internal/models/user.go b/server/internal/models/user.go index 11d6903..3334e71 100644 --- a/server/internal/models/user.go +++ b/server/internal/models/user.go @@ -25,6 +25,7 @@ type User struct { DisplayName string `json:"displayName" db:"display_name"` PasswordHash string `json:"-" db:"password_hash"` Role UserRole `json:"role" db:"role" validate:"required,oneof=admin editor viewer"` + Theme string `json:"theme" db:"theme" validate:"required,oneof=light dark"` CreatedAt time.Time `json:"createdAt" db:"created_at,default"` LastWorkspaceID int `json:"lastWorkspaceId" db:"last_workspace_id"` } diff --git a/server/internal/models/workspace.go b/server/internal/models/workspace.go index 083a14e..fa62927 100644 --- a/server/internal/models/workspace.go +++ b/server/internal/models/workspace.go @@ -13,7 +13,7 @@ type Workspace struct { LastOpenedFilePath string `json:"lastOpenedFilePath" db:"last_opened_file_path"` // Integrated settings - Theme string `json:"theme" db:"theme" validate:"oneof=light dark"` + Theme string `json:"theme" db:"theme" validate:"required,oneof=light dark"` AutoSave bool `json:"autoSave" db:"auto_save"` ShowHiddenFiles bool `json:"showHiddenFiles" db:"show_hidden_files"` GitEnabled bool `json:"gitEnabled" db:"git_enabled"` @@ -40,7 +40,7 @@ func (w *Workspace) ValidateGitSettings() error { func (w *Workspace) SetDefaultSettings() { if w.Theme == "" { - w.Theme = "light" + w.Theme = "dark" } w.AutoSave = w.AutoSave || false