282 lines
8.5 KiB
Go
282 lines
8.5 KiB
Go
package service
|
|
|
|
// PlatformUserService handles business logic for Platform API user endpoints.
|
|
// Reference: Chatwoot Platform::Api::V1::UsersController — AccessToken authenticated
|
|
//
|
|
// These endpoints are distinct from the SuperAdmin-platform routes because they use
|
|
// AccessToken authentication (api_access_token header) instead of JWT+SuperAdmin.
|
|
// The Permissible system governs which resources each PlatformApp can access.
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"gorm.io/datatypes"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// PlatformUserService provides user CRUD for Platform API (AccessToken auth).
|
|
type PlatformUserService struct {
|
|
userRepo *repository.UserRepo
|
|
permissibleRepo *repository.PermissibleRepo
|
|
accessTokenRepo *repository.AccessTokenRepo
|
|
accountUserRepo *repository.AccountUserRepo
|
|
}
|
|
|
|
type PlatformUserRequest struct {
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"display_name"`
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
CustomAttributes map[string]any `json:"custom_attributes"`
|
|
}
|
|
|
|
type PlatformUserResponse struct {
|
|
User model.User
|
|
AccessToken string
|
|
AccountUsers []model.AccountUser
|
|
}
|
|
|
|
// NewPlatformUserService creates a new PlatformUserService.
|
|
func NewPlatformUserService(
|
|
userRepo *repository.UserRepo,
|
|
permissibleRepo *repository.PermissibleRepo,
|
|
extras ...any,
|
|
) *PlatformUserService {
|
|
svc := &PlatformUserService{
|
|
userRepo: userRepo,
|
|
permissibleRepo: permissibleRepo,
|
|
}
|
|
for _, extra := range extras {
|
|
switch repo := extra.(type) {
|
|
case *repository.AccessTokenRepo:
|
|
svc.accessTokenRepo = repo
|
|
case *repository.AccountUserRepo:
|
|
svc.accountUserRepo = repo
|
|
}
|
|
}
|
|
return svc
|
|
}
|
|
|
|
// ValidatePermissible checks that the PlatformApp has permission to access the target user.
|
|
func (s *PlatformUserService) ValidatePermissible(ctx context.Context, platformAppID uint, userID uint) error {
|
|
perm, err := s.permissibleRepo.FindByPlatformAppAndResource(ctx, platformAppID, model.PermissibleTypeUser, userID)
|
|
if err != nil {
|
|
// GORM returns "record not found" when no matching row — treat as non-permissible
|
|
return errors.New("non permissible resource")
|
|
}
|
|
if perm == nil {
|
|
return errors.New("non permissible resource")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetUser retrieves a user by ID, after verifying permissible access.
|
|
func (s *PlatformUserService) GetUser(ctx context.Context, platformAppID uint, userID uint) (*model.User, error) {
|
|
if err := s.ValidatePermissible(ctx, platformAppID, userID); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.userRepo.FindByID(ctx, userID)
|
|
}
|
|
|
|
func (s *PlatformUserService) GetUserResponse(ctx context.Context, platformAppID uint, userID uint) (*PlatformUserResponse, error) {
|
|
user, err := s.GetUser(ctx, platformAppID, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.BuildUserResponse(ctx, user)
|
|
}
|
|
|
|
// CreateUser creates a new user and auto-creates a Permissible record.
|
|
// Reference: Chatwoot UsersController#create — skips confirmation, auto-permissible
|
|
func (s *PlatformUserService) CreateUser(ctx context.Context, platformAppID uint, req PlatformUserRequest) (*PlatformUserResponse, error) {
|
|
user, err := s.userRepo.FindByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
confirmedAt := time.Now().UTC()
|
|
attrs, attrsErr := marshalCustomAttributes(req.CustomAttributes)
|
|
if attrsErr != nil {
|
|
return nil, attrsErr
|
|
}
|
|
user = &model.User{
|
|
Name: req.Name,
|
|
DisplayName: req.DisplayName,
|
|
Email: req.Email,
|
|
Password: req.Password,
|
|
Provider: "email",
|
|
CustomAttributes: attrs,
|
|
ConfirmedAt: &confirmedAt,
|
|
Active: true,
|
|
}
|
|
if err := s.userRepo.Create(ctx, user); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Auto-create Permissible record (PlatformApp can access this user)
|
|
if _, err := s.permissibleRepo.FindByPlatformAppAndResource(ctx, platformAppID, model.PermissibleTypeUser, user.ID); err != nil {
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, err
|
|
}
|
|
perm := &model.Permissible{
|
|
PlatformAppID: platformAppID,
|
|
PermissibleType: model.PermissibleTypeUser,
|
|
PermissibleID: user.ID,
|
|
}
|
|
if err := s.permissibleRepo.Create(ctx, perm); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return s.BuildUserResponse(ctx, user)
|
|
}
|
|
|
|
// UpdateUser updates a user, after verifying permissible access.
|
|
// Reference: Chatwoot UsersController#update — merges custom_attributes, skips reconfirmation
|
|
func (s *PlatformUserService) UpdateUser(ctx context.Context, platformAppID uint, userID uint, req PlatformUserRequest) (*PlatformUserResponse, error) {
|
|
if err := s.ValidatePermissible(ctx, platformAppID, userID); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if req.Name != "" {
|
|
user.Name = req.Name
|
|
}
|
|
if req.DisplayName != "" {
|
|
user.DisplayName = req.DisplayName
|
|
}
|
|
if req.Email != "" {
|
|
user.Email = req.Email
|
|
}
|
|
if req.Password != "" {
|
|
user.Password = req.Password
|
|
}
|
|
if req.CustomAttributes != nil {
|
|
merged := platformJSONObj(user.CustomAttributes)
|
|
for key, value := range req.CustomAttributes {
|
|
merged[key] = value
|
|
}
|
|
attrs, err := marshalCustomAttributes(merged)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
user.CustomAttributes = attrs
|
|
}
|
|
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.BuildUserResponse(ctx, user)
|
|
}
|
|
|
|
func (s *PlatformUserService) BuildUserResponse(ctx context.Context, user *model.User) (*PlatformUserResponse, error) {
|
|
if user == nil {
|
|
return nil, errors.New("user is nil")
|
|
}
|
|
accessToken, err := s.currentAccessToken(ctx, user.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var accountUsers []model.AccountUser
|
|
if s.accountUserRepo != nil {
|
|
accountUsers, err = s.accountUserRepo.FindByUserWithAccounts(ctx, user.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &PlatformUserResponse{User: *user, AccessToken: accessToken, AccountUsers: accountUsers}, nil
|
|
}
|
|
|
|
func (s *PlatformUserService) TokenResponse(ctx context.Context, platformAppID uint, userID uint) (*PlatformUserResponse, error) {
|
|
return s.GetUserResponse(ctx, platformAppID, userID)
|
|
}
|
|
|
|
func (s *PlatformUserService) currentAccessToken(ctx context.Context, userID uint) (string, error) {
|
|
if s.accessTokenRepo == nil {
|
|
return "", nil
|
|
}
|
|
tokens, err := s.accessTokenRepo.FindActiveByOwner(ctx, model.AccessTokenOwnerTypeUser, userID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to load access token: %w", err)
|
|
}
|
|
if len(tokens) > 0 {
|
|
return tokens[0].Token, nil
|
|
}
|
|
plainToken, err := generatePlatformAccessToken()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
accessToken := &model.AccessToken{
|
|
OwnerType: model.AccessTokenOwnerTypeUser,
|
|
OwnerID: userID,
|
|
Token: plainToken,
|
|
TokenPrefix: tokenPrefix(plainToken),
|
|
Name: "Personal Access Token",
|
|
}
|
|
if err := s.accessTokenRepo.Create(ctx, accessToken); err != nil {
|
|
return "", err
|
|
}
|
|
return plainToken, nil
|
|
}
|
|
|
|
// DeleteUser deletes a user, after verifying permissible access.
|
|
// Reference: Chatwoot UsersController#destroy — uses DeleteObjectJob (async)
|
|
func (s *PlatformUserService) DeleteUser(ctx context.Context, platformAppID uint, userID uint) error {
|
|
if err := s.ValidatePermissible(ctx, platformAppID, userID); err != nil {
|
|
return err
|
|
}
|
|
return s.userRepo.Delete(ctx, userID)
|
|
}
|
|
|
|
func marshalCustomAttributes(attrs map[string]any) (datatypes.JSON, error) {
|
|
if attrs == nil {
|
|
return datatypes.JSON([]byte(`{}`)), nil
|
|
}
|
|
data, err := json.Marshal(attrs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return datatypes.JSON(data), nil
|
|
}
|
|
|
|
func platformJSONObj(raw datatypes.JSON) map[string]any {
|
|
if len(raw) == 0 {
|
|
return map[string]any{}
|
|
}
|
|
var obj map[string]any
|
|
if err := json.Unmarshal(raw, &obj); err != nil || obj == nil {
|
|
return map[string]any{}
|
|
}
|
|
return obj
|
|
}
|
|
|
|
// ListPermissibleUsers returns all users that the PlatformApp has permissible access to.
|
|
func (s *PlatformUserService) ListPermissibleUsers(ctx context.Context, platformAppID uint) ([]model.User, error) {
|
|
permissibles, err := s.permissibleRepo.FindByPlatformAppID(ctx, platformAppID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var users []model.User
|
|
for _, perm := range permissibles {
|
|
if perm.PermissibleType == model.PermissibleTypeUser {
|
|
user, err := s.userRepo.FindByID(ctx, perm.PermissibleID)
|
|
if err != nil {
|
|
continue // skip missing users
|
|
}
|
|
users = append(users, *user)
|
|
}
|
|
}
|
|
return users, nil
|
|
}
|