package handlers import ( "database/sql" "encoding/json" "net/http" "strings" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "git.gobha.me/xcaliber/chat-switchboard/database" ) // ── Request Types ─────────────────────────── type updateProfileRequest struct { DisplayName *string `json:"display_name,omitempty"` Email *string `json:"email,omitempty"` } type changePasswordRequest struct { CurrentPassword string `json:"current_password" binding:"required"` NewPassword string `json:"new_password" binding:"required,min=8,max=128"` } type profileResponse struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` DisplayName *string `json:"display_name"` Role string `json:"role"` Avatar *string `json:"avatar,omitempty"` Settings map[string]interface{} `json:"settings"` CreatedAt string `json:"created_at"` } // SettingsHandler manages user profile and preferences. type SettingsHandler struct{} // NewSettingsHandler creates a new handler. func NewSettingsHandler() *SettingsHandler { return &SettingsHandler{} } // ── Get Profile ───────────────────────────── func (h *SettingsHandler) GetProfile(c *gin.Context) { userID := getUserID(c) var p profileResponse var settingsRaw string err := database.DB.QueryRow(` SELECT id, username, email, display_name, role, avatar_url, settings::text, created_at FROM users WHERE id = $1 `, userID).Scan( &p.ID, &p.Username, &p.Email, &p.DisplayName, &p.Role, &p.Avatar, &settingsRaw, &p.CreatedAt, ) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load profile"}) return } p.Settings = make(map[string]interface{}) _ = json.Unmarshal([]byte(settingsRaw), &p.Settings) c.JSON(http.StatusOK, p) } // ── Update Profile ────────────────────────── func (h *SettingsHandler) UpdateProfile(c *gin.Context) { userID := getUserID(c) var req updateProfileRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.DisplayName != nil { _, err := database.DB.Exec( `UPDATE users SET display_name = $1, updated_at = NOW() WHERE id = $2`, *req.DisplayName, userID, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update display name"}) return } } if req.Email != nil { email := strings.ToLower(strings.TrimSpace(*req.Email)) _, err := database.DB.Exec( `UPDATE users SET email = $1, updated_at = NOW() WHERE id = $2`, email, userID, ) if err != nil { if strings.Contains(err.Error(), "duplicate key") { c.JSON(http.StatusConflict, gin.H{"error": "email already taken"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update email"}) return } } h.GetProfile(c) } // ── Change Password ───────────────────────── func (h *SettingsHandler) ChangePassword(c *gin.Context) { userID := getUserID(c) var req changePasswordRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Verify current password var hash string err := database.DB.QueryRow( `SELECT password_hash FROM users WHERE id = $1`, userID, ).Scan(&hash) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to verify password"}) return } if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(req.CurrentPassword)); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "current password is incorrect"}) return } // Hash new password newHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcryptCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"}) return } _, err = database.DB.Exec( `UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2`, string(newHash), userID, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update password"}) return } c.JSON(http.StatusOK, gin.H{"message": "password updated"}) } // ── Get User Settings (JSONB) ─────────────── func (h *SettingsHandler) GetSettings(c *gin.Context) { userID := getUserID(c) var settingsRaw string err := database.DB.QueryRow( `SELECT settings::text FROM users WHERE id = $1`, userID, ).Scan(&settingsRaw) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load settings"}) return } settings := make(map[string]interface{}) _ = json.Unmarshal([]byte(settingsRaw), &settings) c.JSON(http.StatusOK, settings) } // ── Update User Settings (JSONB merge) ────── func (h *SettingsHandler) UpdateSettings(c *gin.Context) { userID := getUserID(c) var incoming map[string]interface{} if err := c.ShouldBindJSON(&incoming); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } patch, err := json.Marshal(incoming) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid settings"}) return } // JSONB merge — existing keys preserved, incoming keys overwrite _, err = database.DB.Exec(` UPDATE users SET settings = settings || $1::jsonb, updated_at = NOW() WHERE id = $2 `, string(patch), userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update settings"}) return } h.GetSettings(c) }