Files
gochat/backend/internal/handler/api/v1/profile_handler.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

298 lines
9.1 KiB
Go

package v1
import (
"encoding/json"
"net/http"
"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
}
// NewProfileHandler creates a new Profile handler.
func NewProfileHandler(svc *service.ProfileService) *ProfileHandler {
return &ProfileHandler{svc: svc}
}
// 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)
}
// 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, err := bindProfileUpdate(c)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
return
}
accountID := c.GetUint("account_id")
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, 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, err
}
return req, nil
}
func bindProfileUpdateForm(c *gin.Context) (service.ProfileUpdatePayload, 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, 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
}
if file, err := c.FormFile("profile[avatar]"); err == nil && file != nil {
profile.AvatarURL = file.Filename
}
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, 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)
}