feat(profile): expose hmac identifier

This commit is contained in:
2026-06-06 11:36:00 +08:00
parent 9c2a396d69
commit fbc0bb833d
4 changed files with 75 additions and 14 deletions
+1 -1
View File
@@ -645,7 +645,7 @@ func Bootstrap(env string) (*App, error) {
// Team + Profile services (P5 — Teams + Team Members + User Profiles)
teamService := service.NewTeamService(teamRepo, teamMemberRepo, db)
profileService := service.NewProfileService(userRepo, accountUserRepo, accessTokenRepo)
profileService := service.NewProfileService(userRepo, accountUserRepo, accessTokenRepo, installationConfigRepo)
// Campaign + AutoAssignment services
campaignInternalSvc := campaign.NewCampaignService(db)
@@ -2,6 +2,9 @@ package v1
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"mime/multipart"
@@ -58,6 +61,7 @@ func (s *ProfileHandlerTestSuite) SetupSuite() {
&model.AccountUser{},
&model.CustomRole{},
&model.AccessToken{},
&model.InstallationConfig{},
))
s.db = db
@@ -94,7 +98,8 @@ func (s *ProfileHandlerTestSuite) SetupSuite() {
userRepo := repository.NewUserRepo(db)
accountUserRepo := repository.NewAccountUserRepo(db)
accessTokenRepo := repository.NewAccessTokenRepo(db)
profileSvc := service.NewProfileService(userRepo, accountUserRepo, accessTokenRepo)
installationConfigRepo := repository.NewInstallationConfigRepo(db)
profileSvc := service.NewProfileService(userRepo, accountUserRepo, accessTokenRepo, installationConfigRepo)
s.handler = NewProfileHandler(profileSvc)
// Build router with profile routes and auth middleware
@@ -148,6 +153,7 @@ func (s *ProfileHandlerTestSuite) SetupTest() {
})
s.db.Unscoped().Where("account_id = ?", s.accountID).Delete(&model.CustomRole{})
s.db.Unscoped().Where("owner_type = ? AND owner_id = ?", model.AccessTokenOwnerTypeUser, s.userID).Delete(&model.AccessToken{})
s.db.Unscoped().Where("name = ?", "CHATWOOT_INBOX_HMAC_KEY").Delete(&model.InstallationConfig{})
s.Require().NoError(s.db.Create(&model.AccessToken{OwnerType: model.AccessTokenOwnerTypeUser, OwnerID: s.userID, Token: "profile-token-1", TokenPrefix: "profile-", Name: "Personal Access Token"}).Error)
}
@@ -182,6 +188,7 @@ func (s *ProfileHandlerTestSuite) TestGet_Success() {
assert.Equal(s.T(), "Profile Display", dataMap["available_name"])
assert.Equal(s.T(), "Regards", dataMap["message_signature"])
assert.Equal(s.T(), "pubsub-profile-user", dataMap["pubsub_token"])
assert.NotContains(s.T(), dataMap, "hmac_identifier")
assert.Equal(s.T(), "administrator", dataMap["role"])
accounts, ok := dataMap["accounts"].([]interface{})
assert.True(s.T(), ok)
@@ -221,6 +228,22 @@ func (s *ProfileHandlerTestSuite) TestGet_CustomRolePermissions() {
assert.Equal(s.T(), []interface{}{"conversation_manage", "contact_manage"}, customRole["permissions"])
}
func (s *ProfileHandlerTestSuite) TestGet_HMACIdentifierWhenConfigured() {
secret := "random_secret_key"
s.Require().NoError(s.db.Create(&model.InstallationConfig{Name: "CHATWOOT_INBOX_HMAC_KEY", Value: secret}).Error)
req, _ := http.NewRequest("GET", "/api/v1/profile", nil)
w := httptest.NewRecorder()
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
payload := s.decodeProfileBody(w)
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte("profile@example.com"))
expected := hex.EncodeToString(mac.Sum(nil))
assert.Equal(s.T(), expected, payload["hmac_identifier"])
}
func (s *ProfileHandlerTestSuite) TestGet_Unauthorized() {
// Create router without auth middleware — user_id will be 0
r := gin.New()
+44 -8
View File
@@ -2,11 +2,16 @@ package service
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
@@ -18,18 +23,24 @@ import (
// ProfileService implements business logic for user profile operations.
// Reference: Chatwoot app/controllers/api/v1/profile_controller.rb
type ProfileService struct {
userRepo *repository.UserRepo
accountUserRepo *repository.AccountUserRepo
accessTokenRepo *repository.AccessTokenRepo
userRepo *repository.UserRepo
accountUserRepo *repository.AccountUserRepo
accessTokenRepo *repository.AccessTokenRepo
installationConfigRepo *repository.InstallationConfigRepo
}
// NewProfileService creates a new Profile service.
func NewProfileService(userRepo *repository.UserRepo, accountUserRepo *repository.AccountUserRepo, accessTokenRepo ...*repository.AccessTokenRepo) *ProfileService {
var tokenRepo *repository.AccessTokenRepo
if len(accessTokenRepo) > 0 {
tokenRepo = accessTokenRepo[0]
func NewProfileService(userRepo *repository.UserRepo, accountUserRepo *repository.AccountUserRepo, extras ...any) *ProfileService {
svc := &ProfileService{userRepo: userRepo, accountUserRepo: accountUserRepo}
for _, extra := range extras {
switch repo := extra.(type) {
case *repository.AccessTokenRepo:
svc.accessTokenRepo = repo
case *repository.InstallationConfigRepo:
svc.installationConfigRepo = repo
}
}
return &ProfileService{userRepo: userRepo, accountUserRepo: accountUserRepo, accessTokenRepo: tokenRepo}
return svc
}
// ProfileUserResponse matches Chatwoot app/views/api/v1/models/_user.json.jbuilder.
@@ -42,6 +53,7 @@ type ProfileUserResponse struct {
DisplayName string `json:"display_name"`
MessageSignature string `json:"message_signature"`
Email string `json:"email"`
HMACIdentifier string `json:"hmac_identifier,omitempty"`
ID uint `json:"id"`
InviterID *uint `json:"inviter_id"`
Name string `json:"name"`
@@ -341,6 +353,10 @@ func (s *ProfileService) serializeUser(ctx context.Context, user *model.User, ac
if err != nil {
return nil, err
}
hmacIdentifier, err := s.hmacIdentifier(ctx, user.Email)
if err != nil {
return nil, err
}
displayName := user.DisplayName
availableName := user.Name
@@ -375,6 +391,7 @@ func (s *ProfileService) serializeUser(ctx context.Context, user *model.User, ac
DisplayName: displayName,
MessageSignature: user.MessageSignature,
Email: user.Email,
HMACIdentifier: hmacIdentifier,
ID: user.ID,
InviterID: inviterID,
Name: user.Name,
@@ -389,6 +406,25 @@ func (s *ProfileService) serializeUser(ctx context.Context, user *model.User, ac
}, nil
}
func (s *ProfileService) hmacIdentifier(ctx context.Context, email string) (string, error) {
if s.installationConfigRepo == nil {
return "", nil
}
cfg, err := s.installationConfigRepo.FindByName(ctx, "CHATWOOT_INBOX_HMAC_KEY")
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", nil
}
return "", err
}
if cfg.Value == "" {
return "", nil
}
mac := hmac.New(sha256.New, []byte(cfg.Value))
_, _ = mac.Write([]byte(email))
return hex.EncodeToString(mac.Sum(nil)), nil
}
func selectActiveAccountUser(accountUsers []model.AccountUser, accountID uint) *model.AccountUser {
if len(accountUsers) == 0 {
return nil