Add theme to user preferences

This commit is contained in:
2025-10-28 20:05:12 +01:00
parent 6c408fdfbe
commit 3926954b74
9 changed files with 70 additions and 6 deletions

3
.gitignore vendored
View File

@@ -162,3 +162,6 @@ go.work.sum
main main
*.db *.db
data data
# Feature specifications
spec.md

View File

@@ -118,6 +118,7 @@ func setupAdminUser(database db.Database, storageManager storage.Manager, cfg *C
DisplayName: "Admin", DisplayName: "Admin",
PasswordHash: string(hashedPassword), PasswordHash: string(hashedPassword),
Role: models.RoleAdmin, Role: models.RoleAdmin,
Theme: "dark", // default theme
} }
createdUser, err := database.CreateUser(adminUser) 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", logging.Info("admin user setup completed",
"userId", createdUser.ID, "userId", createdUser.ID,
"workspaceId", createdUser.LastWorkspaceID) "workspaceId", createdUser.LastWorkspaceID,
"theme", createdUser.Theme)
return nil return nil
} }

View File

@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS users (
display_name TEXT, display_name TEXT,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')), 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, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_workspace_id INTEGER last_workspace_id INTEGER
); );
@@ -19,7 +20,7 @@ CREATE TABLE IF NOT EXISTS workspaces (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_opened_file_path TEXT, last_opened_file_path TEXT,
-- Settings fields -- 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, auto_save BOOLEAN NOT NULL DEFAULT FALSE,
git_enabled BOOLEAN NOT NULL DEFAULT FALSE, git_enabled BOOLEAN NOT NULL DEFAULT FALSE,
git_url TEXT, git_url TEXT,

View File

@@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS users (
display_name TEXT, display_name TEXT,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')), 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, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_workspace_id INTEGER last_workspace_id INTEGER
); );
@@ -18,7 +19,7 @@ CREATE TABLE IF NOT EXISTS workspaces (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_opened_file_path TEXT, last_opened_file_path TEXT,
-- Settings fields -- 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, auto_save BOOLEAN NOT NULL DEFAULT 0,
git_enabled BOOLEAN NOT NULL DEFAULT 0, git_enabled BOOLEAN NOT NULL DEFAULT 0,
git_url TEXT, git_url TEXT,

View File

@@ -22,6 +22,7 @@ type CreateUserRequest struct {
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
Password string `json:"password"` Password string `json:"password"`
Role models.UserRole `json:"role"` Role models.UserRole `json:"role"`
Theme string `json:"theme,omitempty"`
} }
// UpdateUserRequest holds the request fields for updating a user // UpdateUserRequest holds the request fields for updating a user
@@ -30,6 +31,7 @@ type UpdateUserRequest struct {
DisplayName string `json:"displayName,omitempty"` DisplayName string `json:"displayName,omitempty"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
Role models.UserRole `json:"role,omitempty"` Role models.UserRole `json:"role,omitempty"`
Theme string `json:"theme,omitempty"`
} }
// WorkspaceStats holds workspace statistics // WorkspaceStats holds workspace statistics
@@ -164,11 +166,24 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
return 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{ user := &models.User{
Email: req.Email, Email: req.Email,
DisplayName: req.DisplayName, DisplayName: req.DisplayName,
PasswordHash: string(hashedPassword), PasswordHash: string(hashedPassword),
Role: req.Role, Role: req.Role,
Theme: theme,
} }
insertedUser, err := h.DB.CreateUser(user) insertedUser, err := h.DB.CreateUser(user)
@@ -196,6 +211,7 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
"newUserID", insertedUser.ID, "newUserID", insertedUser.ID,
"email", insertedUser.Email, "email", insertedUser.Email,
"role", insertedUser.Role, "role", insertedUser.Role,
"theme", insertedUser.Theme,
) )
respondJSON(w, insertedUser) respondJSON(w, insertedUser)
} }
@@ -322,6 +338,17 @@ func (h *Handler) AdminUpdateUser() http.HandlerFunc {
user.Role = req.Role user.Role = req.Role
updates["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 != "" { if req.Password != "" {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {

View File

@@ -16,6 +16,7 @@ type UpdateProfileRequest struct {
Email string `json:"email"` Email string `json:"email"`
CurrentPassword string `json:"currentPassword"` CurrentPassword string `json:"currentPassword"`
NewPassword string `json:"newPassword"` NewPassword string `json:"newPassword"`
Theme string `json:"theme"`
} }
// DeleteAccountRequest represents a user account deletion request // DeleteAccountRequest represents a user account deletion request
@@ -149,6 +150,19 @@ func (h *Handler) UpdateProfile() http.HandlerFunc {
updates["displayNameChanged"] = true 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 // Update user in database
if err := h.DB.UpdateUser(user); err != nil { if err := h.DB.UpdateUser(user); err != nil {
log.Error("failed to update user in database", log.Error("failed to update user in database",

View File

@@ -87,7 +87,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
"clientIP", r.RemoteAddr, "clientIP", r.RemoteAddr,
) )
var workspace models.Workspace var workspace models.Workspace
if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil { if err := json.NewDecoder(r.Body).Decode(&workspace); err != nil {
log.Debug("invalid request body received", log.Debug("invalid request body received",
"error", err.Error(), "error", err.Error(),
@@ -104,7 +104,21 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
return 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 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 { if err := h.DB.CreateWorkspace(&workspace); err != nil {
log.Error("failed to create workspace in database", log.Error("failed to create workspace in database",
"error", err.Error(), "error", err.Error(),
@@ -145,6 +159,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc {
log.Info("workspace created", log.Info("workspace created",
"workspaceID", workspace.ID, "workspaceID", workspace.ID,
"workspaceName", workspace.Name, "workspaceName", workspace.Name,
"theme", workspace.Theme,
"gitEnabled", workspace.GitEnabled, "gitEnabled", workspace.GitEnabled,
) )
respondJSON(w, workspace) respondJSON(w, workspace)

View File

@@ -25,6 +25,7 @@ type User struct {
DisplayName string `json:"displayName" db:"display_name"` DisplayName string `json:"displayName" db:"display_name"`
PasswordHash string `json:"-" db:"password_hash"` PasswordHash string `json:"-" db:"password_hash"`
Role UserRole `json:"role" db:"role" validate:"required,oneof=admin editor viewer"` 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"` CreatedAt time.Time `json:"createdAt" db:"created_at,default"`
LastWorkspaceID int `json:"lastWorkspaceId" db:"last_workspace_id"` LastWorkspaceID int `json:"lastWorkspaceId" db:"last_workspace_id"`
} }

View File

@@ -13,7 +13,7 @@ type Workspace struct {
LastOpenedFilePath string `json:"lastOpenedFilePath" db:"last_opened_file_path"` LastOpenedFilePath string `json:"lastOpenedFilePath" db:"last_opened_file_path"`
// Integrated settings // 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"` AutoSave bool `json:"autoSave" db:"auto_save"`
ShowHiddenFiles bool `json:"showHiddenFiles" db:"show_hidden_files"` ShowHiddenFiles bool `json:"showHiddenFiles" db:"show_hidden_files"`
GitEnabled bool `json:"gitEnabled" db:"git_enabled"` GitEnabled bool `json:"gitEnabled" db:"git_enabled"`
@@ -40,7 +40,7 @@ func (w *Workspace) ValidateGitSettings() error {
func (w *Workspace) SetDefaultSettings() { func (w *Workspace) SetDefaultSettings() {
if w.Theme == "" { if w.Theme == "" {
w.Theme = "light" w.Theme = "dark"
} }
w.AutoSave = w.AutoSave || false w.AutoSave = w.AutoSave || false