dukcapil/internal/processor/user_processor.go
2025-10-02 22:28:15 +07:00

399 lines
12 KiB
Go

package processor
import (
"context"
"fmt"
"golang.org/x/crypto/bcrypt"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"eslogad-be/internal/transformer"
"github.com/google/uuid"
)
type UserProcessorImpl struct {
userRepo UserRepository
profileRepo UserProfileRepository
novuProcessor NovuProcessor
userRoleProc UserRoleProcessor
}
type UserProfileRepository interface {
GetByUserID(ctx context.Context, userID uuid.UUID) (*entities.UserProfile, error)
Create(ctx context.Context, profile *entities.UserProfile) error
Upsert(ctx context.Context, profile *entities.UserProfile) error
Update(ctx context.Context, profile *entities.UserProfile) error
}
func NewUserProcessor(
userRepo UserRepository,
profileRepo UserProfileRepository,
userRoleProc UserRoleProcessor,
) *UserProcessorImpl {
return &UserProcessorImpl{
userRepo: userRepo,
profileRepo: profileRepo,
userRoleProc: userRoleProc,
}
}
func (p *UserProcessorImpl) SetNovuProcessor(novuProcessor NovuProcessor) {
p.novuProcessor = novuProcessor
}
func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error) {
existingUser, err := p.userRepo.GetByEmail(ctx, req.Email)
if err == nil && existingUser != nil {
return nil, fmt.Errorf("user with email %s already exists", req.Email)
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("failed to hash password: %w", err)
}
userEntity := transformer.CreateUserRequestToEntity(req, string(passwordHash))
err = p.userRepo.Create(ctx, userEntity)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
defaultFullName := userEntity.Name
profile := &entities.UserProfile{
UserID: userEntity.ID,
FullName: defaultFullName,
Timezone: "Asia/Jakarta",
Locale: "id-ID",
Preferences: entities.JSONB{},
NotificationPrefs: entities.JSONB{},
}
_ = p.profileRepo.Create(ctx, profile)
if req.RoleID != nil {
if err := p.userRoleProc.AssignRoleToUser(ctx, userEntity.ID, *req.RoleID); err != nil {
return nil, fmt.Errorf("failed to assign role to user: %w", err)
}
}
// Assign departments if provided
if len(req.DepartmentIDs) > 0 {
departments := make([]entities.Department, len(req.DepartmentIDs))
for i, deptID := range req.DepartmentIDs {
departments[i] = entities.Department{ID: deptID}
}
if err := p.userRepo.UpdateDepartments(ctx, userEntity.ID, departments); err != nil {
return nil, fmt.Errorf("failed to assign departments: %w", err)
}
}
if p.novuProcessor != nil {
if err := p.novuProcessor.CreateSubscriber(ctx, userEntity); err != nil {
_ = err
}
}
// Fetch the user with departments for response
userWithDepts, _ := p.userRepo.GetByIDWithDepartments(ctx, userEntity.ID)
if userWithDepts != nil {
userEntity = userWithDepts
}
return transformer.EntityToContract(userEntity), nil
}
func (p *UserProcessorImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error) {
existingUser, err := p.userRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
if req.Email != nil && *req.Email != existingUser.Email {
existingUserByEmail, err := p.userRepo.GetByEmail(ctx, *req.Email)
if err == nil && existingUserByEmail != nil && existingUserByEmail.ID != id {
return nil, fmt.Errorf("user with email %s already exists", *req.Email)
}
}
updated := transformer.UpdateUserEntity(existingUser, req)
fmt.Printf("Test Updated: %+v\n", updated)
err = p.userRepo.Update(ctx, updated)
if err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
if(req.Name != nil) {
profile, err := p.profileRepo.GetByUserID(ctx, updated.ID)
if err != nil {
return nil, fmt.Errorf("failed to get user profile: %w", err)
}
profile.FullName = *req.Name
if err := p.profileRepo.Update(ctx, profile); err != nil {
return nil, fmt.Errorf("failed to update user profile: %w", err)
}
}
// Update departments if provided
if req.DepartmentIDs != nil {
departments := make([]entities.Department, len(*req.DepartmentIDs))
for i, deptID := range *req.DepartmentIDs {
departments[i] = entities.Department{ID: deptID}
}
if err := p.userRepo.UpdateDepartments(ctx, updated.ID, departments); err != nil {
return nil, fmt.Errorf("failed to update departments: %w", err)
}
}
// Update Novu subscriber
if p.novuProcessor != nil {
if err := p.novuProcessor.UpdateSubscriber(ctx, updated); err != nil {
_ = err
}
}
// Fetch the user with departments for response
userWithDepts, _ := p.userRepo.GetByIDWithDepartments(ctx, updated.ID)
if userWithDepts != nil {
updated = userWithDepts
}
return transformer.EntityToContract(updated), nil
}
func (p *UserProcessorImpl) DeleteUser(ctx context.Context, id uuid.UUID) error {
_, err := p.userRepo.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("user not found: %w", err)
}
err = p.userRepo.Delete(ctx, id)
if err != nil {
return fmt.Errorf("failed to delete user: %w", err)
}
// Delete Novu subscriber
if p.novuProcessor != nil {
if err := p.novuProcessor.DeleteSubscriber(ctx, id); err != nil {
// Log error but don't fail user deletion
_ = err
}
}
return nil
}
func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
user, err := p.userRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
resp := transformer.EntityToContract(user)
if resp != nil {
// Roles are loaded separately since they're not preloaded
if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil {
resp.Roles = transformer.RolesToContract(roles)
}
// Departments are now preloaded, so they're already in the response
}
return resp, nil
}
// GetUserByIDLight retrieves user without relationships - optimized for auth checks
func (p *UserProcessorImpl) GetUserByIDLight(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error) {
user, err := p.userRepo.GetByIDLight(ctx, id)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
resp := &contract.UserResponse{
ID: user.ID,
Email: user.Email,
Name: user.Name,
IsActive: user.IsActive,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
return resp, nil
}
func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) {
user, err := p.userRepo.GetByEmail(ctx, email)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
// Departments are now preloaded, so they're already in the response
return transformer.EntityToContract(user), nil
}
func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]contract.UserResponse, int, error) {
users, totalCount, err := p.userRepo.ListWithFilters(ctx, search, roleCode, isActive, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to get users: %w", err)
}
responses := transformer.EntitiesToContracts(users)
userIDs := make([]uuid.UUID, 0, len(responses))
for i := range responses {
userIDs = append(userIDs, responses[i].ID)
}
// Roles are loaded separately since they're not preloaded
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
if err == nil {
for i := range responses {
if roles, ok := rolesMap[responses[i].ID]; ok {
responses[i].Roles = transformer.RolesToContract(roles)
}
}
}
// Departments are now preloaded, so they're already in the responses
return responses, int(totalCount), nil
}
func (p *UserProcessorImpl) GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error) {
user, err := p.userRepo.GetByEmail(ctx, email)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
return user, nil
}
func (p *UserProcessorImpl) ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error {
user, err := p.userRepo.GetByID(ctx, userID)
if err != nil {
return fmt.Errorf("user not found: %w", err)
}
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword))
if err != nil {
return fmt.Errorf("current password is incorrect")
}
newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("failed to hash new password: %w", err)
}
err = p.userRepo.UpdatePassword(ctx, userID, string(newPasswordHash))
if err != nil {
return fmt.Errorf("failed to update password: %w", err)
}
return nil
}
func (p *UserProcessorImpl) ActivateUser(ctx context.Context, userID uuid.UUID) error {
_, err := p.userRepo.GetByID(ctx, userID)
if err != nil {
return fmt.Errorf("user not found: %w", err)
}
err = p.userRepo.UpdateActiveStatus(ctx, userID, true)
if err != nil {
return fmt.Errorf("failed to activate user: %w", err)
}
return nil
}
func (p *UserProcessorImpl) DeactivateUser(ctx context.Context, userID uuid.UUID) error {
_, err := p.userRepo.GetByID(ctx, userID)
if err != nil {
return fmt.Errorf("user not found: %w", err)
}
err = p.userRepo.UpdateActiveStatus(ctx, userID, false)
if err != nil {
return fmt.Errorf("failed to deactivate user: %w", err)
}
return nil
}
// RBAC implementations
func (p *UserProcessorImpl) GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, error) {
roles, err := p.userRepo.GetRolesByUserID(ctx, userID)
if err != nil {
return nil, err
}
return transformer.RolesToContract(roles), nil
}
func (p *UserProcessorImpl) GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error) {
perms, err := p.userRepo.GetPermissionsByUserID(ctx, userID)
if err != nil {
return nil, err
}
codes := make([]string, 0, len(perms))
for _, p := range perms {
codes = append(codes, p.Code)
}
return codes, nil
}
func (p *UserProcessorImpl) GetUserDepartments(ctx context.Context, userID uuid.UUID) ([]contract.DepartmentResponse, error) {
departments, err := p.userRepo.GetDepartmentsByUserID(ctx, userID)
if err != nil {
return nil, err
}
return transformer.DepartmentsToContract(departments), nil
}
func (p *UserProcessorImpl) GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {
prof, err := p.profileRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
}
return transformer.ProfileEntityToContract(prof), nil
}
func (p *UserProcessorImpl) UpdateUserProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) {
existing, _ := p.profileRepo.GetByUserID(ctx, userID)
entity := transformer.ProfileUpdateToEntity(userID, req, existing)
if existing == nil {
if err := p.profileRepo.Create(ctx, entity); err != nil {
return nil, err
}
} else {
if err := p.profileRepo.Update(ctx, entity); err != nil {
return nil, err
}
}
return transformer.ProfileEntityToContract(entity), nil
}
// GetActiveUsersForMention retrieves active users for mention purposes with optional username search
func (p *UserProcessorImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
// Limit validation is handled in the service layer
// Set isActive to true to only get active users
isActive := true
users, _, err := p.userRepo.ListWithFilters(ctx, search, nil, &isActive, limit, 0)
if err != nil {
return nil, fmt.Errorf("failed to get active users: %w", err)
}
responses := transformer.EntitiesToContracts(users)
userIDs := make([]uuid.UUID, 0, len(responses))
for i := range responses {
userIDs = append(userIDs, responses[i].ID)
}
// Load roles for the users
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
if err == nil {
for i := range responses {
if roles, ok := rolesMap[responses[i].ID]; ok {
responses[i].Roles = transformer.RolesToContract(roles)
}
}
}
return responses, nil
}