226 lines
8.7 KiB
Go
226 lines
8.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
pkgvalidator "github.com/gochat/gochat/pkg/validator"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// NewProfileService creates a new Profile service.
|
|
func NewProfileService(userRepo *repository.UserRepo, accountUserRepo *repository.AccountUserRepo) *ProfileService {
|
|
return &ProfileService{userRepo: userRepo, accountUserRepo: accountUserRepo}
|
|
}
|
|
|
|
// UpdateProfileRequest is the DTO for updating user profile.
|
|
// Reference: Chatwoot profiles_controller#update — params wrapped in "profile" key
|
|
type UpdateProfileRequest struct {
|
|
Name string `json:"name,omitempty" validate:"omitempty,min=2"`
|
|
Email string `json:"email,omitempty" validate:"omitempty,email"`
|
|
AvatarURL string `json:"avatar_url,omitempty"`
|
|
Availability string `json:"availability,omitempty" validate:"omitempty,oneof=online offline busy"`
|
|
}
|
|
|
|
// ProfileUpdatePayload wraps UpdateProfileRequest under the "profile" key,
|
|
// matching Chatwoot's params.require(:profile).permit(:name, :email, ...)
|
|
type ProfileUpdatePayload struct {
|
|
Profile UpdateProfileRequest `json:"profile" validate:"required"`
|
|
}
|
|
|
|
// UpdateAvatarRequest is the DTO for updating user avatar.
|
|
type UpdateAvatarRequest struct {
|
|
AvatarURL string `json:"avatar_url" validate:"required"`
|
|
}
|
|
|
|
// AvailabilityRequest is the DTO for updating user availability per account.
|
|
// Reference: Chatwoot profiles_controller#availability — params wrapped in "profile" key
|
|
type AvailabilityRequest struct {
|
|
AccountID uint `json:"account_id" validate:"required"`
|
|
Availability string `json:"availability" validate:"required,oneof=online offline busy"`
|
|
}
|
|
|
|
// ProfileAvailabilityPayload wraps AvailabilityRequest under the "profile" key,
|
|
// matching Chatwoot's params.require(:profile).permit(:account_id, :availability)
|
|
type ProfileAvailabilityPayload struct {
|
|
Profile AvailabilityRequest `json:"profile" validate:"required"`
|
|
}
|
|
|
|
// AutoOfflineRequest is the DTO for updating auto_offline setting per account.
|
|
// Reference: Chatwoot profiles_controller#auto_offline — params wrapped in "profile" key
|
|
type AutoOfflineRequest struct {
|
|
AccountID uint `json:"account_id" validate:"required"`
|
|
AutoOffline bool `json:"auto_offline"`
|
|
}
|
|
|
|
// ProfileAutoOfflinePayload wraps AutoOfflineRequest under the "profile" key,
|
|
// matching Chatwoot's params.require(:profile).permit(:account_id, :auto_offline)
|
|
type ProfileAutoOfflinePayload struct {
|
|
Profile AutoOfflineRequest `json:"profile" validate:"required"`
|
|
}
|
|
|
|
// SetActiveAccountRequest is the DTO for setting the active account.
|
|
// Reference: Chatwoot profiles_controller#set_active_account — params wrapped in "profile" key
|
|
type SetActiveAccountRequest struct {
|
|
AccountID uint `json:"account_id" validate:"required"`
|
|
}
|
|
|
|
// ProfileSetActiveAccountPayload wraps SetActiveAccountRequest under the "profile" key,
|
|
// matching Chatwoot's params.require(:profile).permit(:account_id)
|
|
type ProfileSetActiveAccountPayload struct {
|
|
Profile SetActiveAccountRequest `json:"profile" validate:"required"`
|
|
}
|
|
|
|
// Get retrieves the current user's profile.
|
|
func (s *ProfileService) Get(ctx context.Context, userID uint) (*model.User, error) {
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// Update updates the current user's profile.
|
|
func (s *ProfileService) Update(ctx context.Context, userID uint, req UpdateProfileRequest) (*model.User, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
|
|
if req.Name != "" {
|
|
user.Name = req.Name
|
|
}
|
|
if req.Email != "" {
|
|
user.Email = req.Email
|
|
}
|
|
if req.AvatarURL != "" {
|
|
user.AvatarURL = req.AvatarURL
|
|
}
|
|
// Map availability to user.Available boolean
|
|
if req.Availability == "online" {
|
|
user.Available = true
|
|
} else if req.Availability == "offline" || req.Availability == "busy" {
|
|
user.Available = false
|
|
}
|
|
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
applogger.L().Errorf("failed to update profile: %v", err)
|
|
return nil, fmt.Errorf("failed to update profile: %w", err)
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// UpdateAvatar updates the current user's avatar URL.
|
|
func (s *ProfileService) UpdateAvatar(ctx context.Context, userID uint, req UpdateAvatarRequest) (*model.User, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
|
|
user.AvatarURL = req.AvatarURL
|
|
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
applogger.L().Errorf("failed to update avatar: %v", err)
|
|
return nil, fmt.Errorf("failed to update avatar: %w", err)
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// SetAvailability updates the user's availability status for a specific account.
|
|
// Reference: Chatwoot profiles_controller#availability — POST /api/v1/profile/availability
|
|
func (s *ProfileService) SetAvailability(ctx context.Context, userID uint, req AvailabilityRequest) (*model.AccountUser, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.accountUserRepo.UpdateAvailability(ctx, req.AccountID, userID, req.Availability); err != nil {
|
|
return nil, fmt.Errorf("failed to update availability: %w", err)
|
|
}
|
|
return s.accountUserRepo.FindByAccountAndUserOrFail(ctx, req.AccountID, userID)
|
|
}
|
|
|
|
// SetAutoOffline updates the user's auto_offline setting for a specific account.
|
|
// Reference: Chatwoot profiles_controller#auto_offline — POST /api/v1/profile/auto_offline
|
|
func (s *ProfileService) SetAutoOffline(ctx context.Context, userID uint, req AutoOfflineRequest) (*model.AccountUser, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.accountUserRepo.UpdateAutoOffline(ctx, req.AccountID, userID, req.AutoOffline); err != nil {
|
|
return nil, fmt.Errorf("failed to update auto_offline: %w", err)
|
|
}
|
|
return s.accountUserRepo.FindByAccountAndUserOrFail(ctx, req.AccountID, userID)
|
|
}
|
|
|
|
// SetActiveAccount updates the active_at timestamp for the user's account membership.
|
|
// Reference: Chatwoot profiles_controller#set_active_account — PUT /api/v1/profile/set_active_account
|
|
func (s *ProfileService) SetActiveAccount(ctx context.Context, userID uint, req SetActiveAccountRequest) error {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return err
|
|
}
|
|
if err := s.accountUserRepo.UpdateActiveAt(ctx, req.AccountID, userID); err != nil {
|
|
return fmt.Errorf("failed to set active account: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ResendConfirmation sends a confirmation email to the user if not yet confirmed.
|
|
// Reference: Chatwoot auth/resend_confirmations_controller#create — POST /api/v1/resend_confirmation
|
|
func (s *ProfileService) ResendConfirmation(ctx context.Context, userID uint) error {
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return fmt.Errorf("user not found: %w", err)
|
|
}
|
|
if user.ConfirmedAt != nil {
|
|
// Already confirmed, do nothing (per chatwoot: skip if confirmed)
|
|
return nil
|
|
}
|
|
// TODO: integrate email sending service for confirmation emails
|
|
applogger.L().Infof("ResendConfirmation called for user %d (%s)", userID, user.Email)
|
|
return nil
|
|
}
|
|
|
|
// ResetAccessToken regenerates the user's access token.
|
|
// Reference: Chatwoot profiles_controller#reset_access_token — POST /api/v1/profile/reset_access_token
|
|
// In chatwoot this regenerates the Doorkeeper OAuth token. In gochat with JWT auth,
|
|
// the "reset" means the current JWT is invalidated and a new one must be obtained.
|
|
func (s *ProfileService) ResetAccessToken(ctx context.Context, userID uint) (*model.User, error) {
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
user.SignInCount++
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
return nil, fmt.Errorf("failed to reset access token: %w", err)
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
// DeleteAvatar removes the user's avatar image.
|
|
// Reference: Chatwoot ProfilesController#destroy_avatar (DELETE :avatar on: :collection)
|
|
func (s *ProfileService) DeleteAvatar(ctx context.Context, userID uint) (*model.User, error) {
|
|
user, err := s.userRepo.FindByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
user.AvatarURL = ""
|
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
|
return nil, fmt.Errorf("failed to remove avatar: %w", err)
|
|
}
|
|
return user, nil
|
|
} |