package v1 import ( "encoding/json" "mime/multipart" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gochat/gochat/pkg/response" ) // ProfileHandler handles Profile get/update + avatar + availability + auto_offline + set_active_account + resend_confirmation + reset_access_token. // Reference: Chatwoot app/controllers/api/v1/profile_controller.rb type ProfileHandler struct { svc *service.ProfileService uploadSvc *service.UploadService } // NewProfileHandler creates a new Profile handler. func NewProfileHandler(svc *service.ProfileService, uploadSvc ...*service.UploadService) *ProfileHandler { handler := &ProfileHandler{svc: svc} if len(uploadSvc) > 0 { handler.uploadSvc = uploadSvc[0] } return handler } // Get returns the current user's profile. // GET /api/v1/profile func (h *ProfileHandler) Get(c *gin.Context) { userID := getUserID(c) if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated") return } accountID := c.GetUint("account_id") user, err := h.svc.Get(c.Request.Context(), userID, accountID) if err != nil { applogger.L().Errorf("Get profile for user %d: %v", userID, err) handleServiceError(c, err) return } c.JSON(http.StatusOK, user) } func (h *ProfileHandler) ListSessions(c *gin.Context) { userID := getUserID(c) sessions, err := h.svc.ListUserSessions(c.Request.Context(), userID) if err != nil { handleServiceError(c, err) return } currentClientID := c.GetHeader("client") payload := make([]gin.H, 0, len(sessions)) for i := range sessions { session := sessions[i] payload = append(payload, gin.H{ "id": session.ID, "browser_name": session.BrowserName, "browser_version": session.BrowserVersion, "device_name": session.DeviceName, "platform_name": session.PlatformName, "platform_version": session.PlatformVersion, "ip_address": session.IPAddress, "city": session.City, "country": session.Country, "country_code": session.CountryCode, "last_activity_at": session.LastActivityAt, "created_at": session.CreatedAt, "current": session.ClientID == currentClientID, }) } c.JSON(http.StatusOK, payload) } func (h *ProfileHandler) RevokeSession(c *gin.Context) { userID := getUserID(c) id, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid session id") return } if err := h.svc.RevokeUserSession(c.Request.Context(), userID, uint(id), c.GetHeader("client")); err != nil { if strings.Contains(strings.ToLower(err.Error()), "current session") { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) return } handleServiceError(c, err) return } c.Status(http.StatusOK) } // Update updates the current user's profile. // PUT /api/v1/profile func (h *ProfileHandler) Update(c *gin.Context) { userID := getUserID(c) if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated") return } req, avatar, err := bindProfileUpdate(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } accountID := c.GetUint("account_id") if avatar != nil { if h.uploadSvc == nil { response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "profile avatar storage is not configured") return } upload, uploadErr := h.uploadSvc.ProfileAvatarUpload(c.Request.Context(), accountID, avatar) if uploadErr != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, uploadErr.Error()) return } req.Profile.AvatarURL = upload.FileURL } user, svcErr := h.svc.Update(c.Request.Context(), userID, accountID, req.Profile) if svcErr != nil { applogger.L().Errorf("Update profile for user %d: %v", userID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, user) } func bindProfileUpdate(c *gin.Context) (service.ProfileUpdatePayload, *multipart.FileHeader, error) { contentType := c.GetHeader("Content-Type") if strings.Contains(contentType, "multipart/form-data") || strings.Contains(contentType, "application/x-www-form-urlencoded") { return bindProfileUpdateForm(c) } var req service.ProfileUpdatePayload if err := c.ShouldBindJSON(&req); err != nil { return req, nil, err } return req, nil, nil } func bindProfileUpdateForm(c *gin.Context) (service.ProfileUpdatePayload, *multipart.FileHeader, error) { var req service.ProfileUpdatePayload if err := c.Request.ParseMultipartForm(32 << 20); err != nil && !strings.Contains(err.Error(), "request Content-Type isn't multipart/form-data") { return req, nil, err } form := c.Request.Form profile := &req.Profile if value := form.Get("profile[name]"); value != "" { profile.Name = value } if value := form.Get("profile[email]"); value != "" { profile.Email = value } if _, ok := form["profile[display_name]"]; ok { value := form.Get("profile[display_name]") profile.DisplayName = &value } if _, ok := form["profile[message_signature]"]; ok { value := form.Get("profile[message_signature]") profile.MessageSignature = &value } if _, ok := form["profile[phone_number]"]; ok { value := form.Get("profile[phone_number]") profile.PhoneNumber = &value } if value := form.Get("profile[avatar_url]"); value != "" { profile.AvatarURL = value } avatar, _ := c.FormFile("profile[avatar]") uiSettings := map[string]any{} for key, values := range form { if !strings.HasPrefix(key, "profile[ui_settings][") || len(values) == 0 { continue } settingKey := strings.TrimSuffix(strings.TrimPrefix(key, "profile[ui_settings]["), "]") uiSettings[settingKey] = values[0] } if raw := form.Get("profile[ui_settings]"); raw != "" { var parsed map[string]any if err := json.Unmarshal([]byte(raw), &parsed); err == nil { uiSettings = parsed } } if len(uiSettings) > 0 { profile.UISettings = uiSettings } return req, avatar, nil } // UpdateAvatar updates the current user's avatar. // PUT /api/v1/profile/avatar func (h *ProfileHandler) UpdateAvatar(c *gin.Context) { userID := getUserID(c) if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated") return } var req service.UpdateAvatarRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } accountID := c.GetUint("account_id") user, svcErr := h.svc.UpdateAvatar(c.Request.Context(), userID, accountID, req) if svcErr != nil { applogger.L().Errorf("Update avatar for user %d: %v", userID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, user) } // SetAvailability updates the user's availability status for a specific account. // POST /api/v1/profile/availability // Reference: Chatwoot profiles_controller#availability func (h *ProfileHandler) SetAvailability(c *gin.Context) { userID := getUserID(c) if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated") return } var req service.ProfileAvailabilityPayload if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } user, svcErr := h.svc.SetAvailability(c.Request.Context(), userID, req.Profile) if svcErr != nil { applogger.L().Errorf("SetAvailability for user %d: %v", userID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, user) } // SetAutoOffline updates the user's auto_offline setting for a specific account. // POST /api/v1/profile/auto_offline // Reference: Chatwoot profiles_controller#auto_offline func (h *ProfileHandler) SetAutoOffline(c *gin.Context) { userID := getUserID(c) if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated") return } var req service.ProfileAutoOfflinePayload if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } user, svcErr := h.svc.SetAutoOffline(c.Request.Context(), userID, req.Profile) if svcErr != nil { applogger.L().Errorf("SetAutoOffline for user %d: %v", userID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, user) } // SetActiveAccount sets the user's currently active account. // PUT /api/v1/profile/set_active_account // Reference: Chatwoot profiles_controller#set_active_account func (h *ProfileHandler) SetActiveAccount(c *gin.Context) { userID := getUserID(c) if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated") return } var req service.ProfileSetActiveAccountPayload if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } svcErr := h.svc.SetActiveAccount(c.Request.Context(), userID, req.Profile) if svcErr != nil { applogger.L().Errorf("SetActiveAccount for user %d: %v", userID, svcErr) handleServiceError(c, svcErr) return } response.NoContent(c) } // ResendConfirmation sends a confirmation email to the user if not yet confirmed. // POST /api/v1/profile/resend_confirmation // Reference: Chatwoot auth/resend_confirmations_controller#create func (h *ProfileHandler) ResendConfirmation(c *gin.Context) { userID := getUserID(c) if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated") return } svcErr := h.svc.ResendConfirmation(c.Request.Context(), userID) if svcErr != nil { applogger.L().Errorf("ResendConfirmation for user %d: %v", userID, svcErr) handleServiceError(c, svcErr) return } response.NoContent(c) } // ResetAccessToken regenerates the user's access token, invalidating current JWTs. // POST /api/v1/profile/reset_access_token // Reference: Chatwoot profiles_controller#reset_access_token func (h *ProfileHandler) ResetAccessToken(c *gin.Context) { userID := getUserID(c) if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated") return } accountID := c.GetUint("account_id") user, svcErr := h.svc.ResetAccessToken(c.Request.Context(), userID, accountID) if svcErr != nil { applogger.L().Errorf("ResetAccessToken for user %d: %v", userID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, user) } // DeleteAvatar removes the user's avatar. // DELETE /api/v1/profile/avatar // Reference: Chatwoot ProfilesController#destroy_avatar func (h *ProfileHandler) DeleteAvatar(c *gin.Context) { userID := getUserID(c) if userID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "user not authenticated") return } accountID := c.GetUint("account_id") user, err := h.svc.DeleteAvatar(c.Request.Context(), userID, accountID) if err != nil { applogger.L().Errorf("DeleteAvatar for user %d: %v", userID, err) handleServiceError(c, err) return } c.JSON(http.StatusOK, user) }