feat(profile): expose hmac identifier
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user