Files
gochat/internal/service/profile_service.go
T
2026-06-04 15:44:48 +08:00

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
}