package services import ( "context" "fmt" "history-api/internal/dtos/request" "history-api/internal/dtos/response" "history-api/internal/gen/sqlc" "history-api/internal/models" "history-api/internal/repositories" "history-api/pkg/cache" "history-api/pkg/constants" "history-api/pkg/convert" "slices" "github.com/gofiber/fiber/v3" "github.com/jackc/pgx/v5/pgtype" "golang.org/x/crypto/bcrypt" "golang.org/x/sync/errgroup" ) type UserService interface { //user GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error) UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error) ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error //admin DeleteUser(ctx context.Context, userId string) error ChangeRoleUser(ctx context.Context, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, error) RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error) SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) } type userService struct { userRepo repositories.UserRepository roleRepo repositories.RoleRepository c cache.Cache } func NewUserService( userRepo repositories.UserRepository, roleRepo repositories.RoleRepository, c cache.Cache, ) UserService { return &userService{ userRepo: userRepo, roleRepo: roleRepo, c: c, } } func (u *userService) ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error { pgID, err := convert.StringToUUID(userId) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } user, err := u.userRepo.GetByID(ctx, pgID) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } if user == nil { return fiber.NewError(fiber.StatusNotFound, "User not found") } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.OldPassword)); err != nil { return fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!") } hashPassword, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } err = u.userRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{ ID: pgID, PasswordHash: pgtype.Text{String: string(hashPassword), Valid: true}, }) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } return nil } func (u *userService) ChangeRoleUser(ctx context.Context, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, error) { userId, err := convert.StringToUUID(dto.UserID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } user, err := u.userRepo.GetByID(ctx, userId) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, err.Error()) } if user == nil { return nil, fiber.NewError(fiber.StatusNotFound, "User not found") } newListRole, err := u.roleRepo.GetByIDs(ctx, dto.Roles) if err != nil { return nil, err } hasUserRole := false hasAdminRole := false hasBannedRole := false hasModRole := false for _, r := range newListRole { if r.Name == constants.USER.String() { hasUserRole = true } if r.Name == constants.ADMIN.String() { hasAdminRole = true } if r.Name == constants.BANNED.String() { hasBannedRole = true } if r.Name == constants.MOD.String() { hasModRole = true } } if !hasUserRole { return nil, fiber.NewError(fiber.StatusNotFound, "User must have the USER role") } if slices.Contains(claims.Roles, constants.MOD) && !slices.Contains(claims.Roles, constants.ADMIN) { if hasAdminRole { return nil, fiber.NewError(fiber.StatusForbidden, "MOD cannot assign ADMIN role to any user") } if dto.UserID == claims.UId && !hasModRole { return nil, fiber.NewError(fiber.StatusForbidden, "You can't remove MOD role of yourself") } if dto.UserID == claims.UId && hasBannedRole { return nil, fiber.NewError(fiber.StatusForbidden, "You can't assign BANNED role to yourself") } isTargetAdminOrMod := false for _, r := range user.Roles { if r.Name == constants.ADMIN.String() || r.Name == constants.MOD.String() { isTargetAdminOrMod = true break } } if isTargetAdminOrMod && hasBannedRole { return nil, fiber.NewError(fiber.StatusForbidden, "MOD cannot assign BANNED role to an ADMIN or MOD user") } } if slices.Contains(claims.Roles, constants.ADMIN) { if dto.UserID == claims.UId && hasBannedRole { return nil, fiber.NewError(fiber.StatusForbidden, "You can't assign BANNED role to yourself") } if dto.UserID == claims.UId && !hasAdminRole { return nil, fiber.NewError(fiber.StatusForbidden, "You can't remove ADMIN role of yourself") } } user.Roles = make([]*models.RoleSimple, 0) roleIdList := make([]pgtype.UUID, 0) for _, role := range newListRole { roleID, err := convert.StringToUUID(role.ID) if err != nil { continue } roleIdList = append(roleIdList, roleID) user.Roles = append(user.Roles, role.ToRoleSimple()) } err = u.roleRepo.BulkDeleteRolesFromUser(ctx, userId) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } err = u.roleRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{ UserID: userId, Column2: roleIdList, }) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } err = u.userRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{ ID: userId, TokenVersion: user.TokenVersion + 1, }) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } user.TokenVersion += 1 mapCache := map[string]any{ fmt.Sprintf("user:email:%s", user.Email): user, fmt.Sprintf("user:id:%s", user.ID): user, } _ = u.c.MSet(ctx, mapCache, constants.NormalCacheDuration) return user.ToResponse(), nil } func (u *userService) DeleteUser(ctx context.Context, userId string) error { pgID, err := convert.StringToUUID(userId) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } user, err := u.userRepo.GetByID(ctx, pgID) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } if user == nil { return fiber.NewError(fiber.StatusNotFound, "User not found") } err = u.userRepo.Delete(ctx, pgID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } return nil } func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error) { pgID, err := convert.StringToUUID(userId) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } user, err := u.userRepo.GetByID(ctx, pgID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, err.Error()) } if user == nil { return nil, fiber.NewError(fiber.StatusNotFound, "User not found") } newUser, err := u.userRepo.UpdateProfile( ctx, sqlc.UpdateUserProfileParams{ DisplayName: convert.PtrToText(dto.DisplayName), FullName: convert.PtrToText(dto.FullName), AvatarUrl: convert.PtrToText(dto.AvatarUrl), Bio: convert.PtrToText(dto.Bio), Location: convert.PtrToText(dto.Location), Website: convert.PtrToText(dto.Website), CountryCode: convert.PtrToText(dto.CountryCode), Phone: convert.PtrToText(dto.Phone), UserID: pgID, }, ) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } return newUser.ToResponse(), nil } func (u *userService) GetUserCurrent(ctx context.Context, userId string) (*response.UserResponse, error) { pgID, err := convert.StringToUUID(userId) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } user, err := u.userRepo.GetByID(ctx, pgID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, err.Error()) } return user.ToResponse(), nil } func (u *userService) RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error) { pgID, err := convert.StringToUUID(userId) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } user, err := u.userRepo.GetByIDWithoutDeleted(ctx, pgID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, err.Error()) } if user == nil { return nil, fiber.NewError(fiber.StatusNotFound, "User not found") } err = u.userRepo.Restore(ctx, pgID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } user.IsDeleted = false return user.ToResponse(), nil } func (m *userService) fillSearchArgs(arg *sqlc.SearchUsersParams, dto *request.SearchUserDto) { if dto.Sort != "" { arg.Sort = pgtype.Text{String: dto.Sort, Valid: true} } else { arg.Sort = pgtype.Text{String: "id", Valid: true} } arg.Order = pgtype.Text{String: "asc", Valid: true} if dto.Order == "desc" { arg.Order = pgtype.Text{String: "desc", Valid: true} } if dto.AuthProvider != "" { arg.AuthProvider = pgtype.Text{String: dto.AuthProvider, Valid: true} } if dto.CreatedFrom != nil { arg.CreatedFrom = pgtype.Timestamptz{Time: *dto.CreatedFrom, Valid: true} } if dto.CreatedTo != nil { arg.CreatedTo = pgtype.Timestamptz{Time: *dto.CreatedTo, Valid: true} } if dto.IsDeleted != nil { arg.IsDeleted = pgtype.Bool{Bool: *dto.IsDeleted, Valid: true} } if len(dto.RoleIDs) > 0 { for _, id := range dto.RoleIDs { if u, err := convert.StringToUUID(id); err == nil { arg.RoleIds = append(arg.RoleIds, u) } } } if dto.Search != "" { arg.SearchText = pgtype.Text{String: dto.Search, Valid: true} } } func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) { if dto.Page < 1 { dto.Page = 1 } if dto.Limit == 0 { dto.Limit = 20 } offset := (dto.Page - 1) * dto.Limit arg := sqlc.SearchUsersParams{ Limit: int32(dto.Limit), Offset: int32(offset), } u.fillSearchArgs(&arg, dto) var rows []*models.UserEntity var totalRecords int64 g, gCtx := errgroup.WithContext(ctx) g.Go(func() error { var err error rows, err = u.userRepo.Search(gCtx, arg) return err }) g.Go(func() error { countArg := sqlc.CountUsersParams{ RoleIds: arg.RoleIds, AuthProvider: arg.AuthProvider, CreatedFrom: arg.CreatedFrom, CreatedTo: arg.CreatedTo, IsDeleted: arg.IsDeleted, SearchText: arg.SearchText, } var err error totalRecords, err = u.userRepo.Count(gCtx, countArg) return err }) if err := g.Wait(); err != nil { return nil, err } users := models.UsersEntityToResponse(rows) return response.BuildPaginatedResponse(users, totalRecords, dto.Page, dto.Limit), nil } func (u *userService) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error) { pgID, err := convert.StringToUUID(userId) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } user, err := u.userRepo.GetByID(ctx, pgID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, err.Error()) } return user.ToResponse(), nil }