354 lines
11 KiB
Go
354 lines
11 KiB
Go
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)
|
|
}
|