UPDATE: Fix bug
All checks were successful
Build and Release / release (push) Successful in 1m8s

This commit is contained in:
2026-04-27 20:31:01 +07:00
parent eb08c16232
commit 17aafacbfd
36 changed files with 842 additions and 447 deletions

View File

@@ -2,6 +2,8 @@ package services
import (
"context"
"database/sql"
"errors"
"fmt"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
@@ -22,15 +24,16 @@ import (
type UserService interface {
//user
UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error)
ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error
UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, *fiber.Error)
ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) *fiber.Error
//admin
DeleteUser(ctx context.Context, userId string) error
ChangeRoleUser(ctx context.Context, userId string, 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)
CreateUser(ctx context.Context, dto *request.CreateUserDto) (*response.UserResponse, *fiber.Error)
DeleteUser(ctx context.Context, userId string) *fiber.Error
ChangeRoleUser(ctx context.Context, userId string, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, *fiber.Error)
RestoreUser(ctx context.Context, userId string) (*response.UserResponse, *fiber.Error)
GetUserByID(ctx context.Context, userId string) (*response.UserResponse, *fiber.Error)
SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, *fiber.Error)
}
type userService struct {
@@ -45,7 +48,6 @@ func NewUserService(
roleRepo repositories.RoleRepository,
c cache.Cache,
db *pgxpool.Pool,
) UserService {
return &userService{
userRepo: userRepo,
@@ -55,7 +57,80 @@ func NewUserService(
}
}
func (u *userService) ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) error {
func (u *userService) CreateUser(ctx context.Context, dto *request.CreateUserDto) (*response.UserResponse, *fiber.Error) {
tx, err := u.db.Begin(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
defer tx.Rollback(ctx)
uRepo := u.userRepo.WithTx(tx)
rRepo := u.roleRepo.WithTx(tx)
existingUser, err := u.userRepo.GetByEmail(ctx, dto.Email)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing user")
}
if existingUser != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "User already exists")
}
hashed, err := bcrypt.GenerateFromPassword([]byte(dto.Password), bcrypt.DefaultCost)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to hash password")
}
user, err := uRepo.UpsertUser(ctx, sqlc.UpsertUserParams{
Email: dto.Email,
PasswordHash: pgtype.Text{String: string(hashed), Valid: true},
AuthProvider: constants.ProviderTypeLocal.String(),
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create user")
}
userUUID, err := convert.StringToUUID(user.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
}
_, err = uRepo.CreateProfile(ctx, sqlc.CreateUserProfileParams{
UserID: userUUID,
DisplayName: pgtype.Text{String: dto.DisplayName, Valid: true},
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create user profile")
}
var roleIdList []pgtype.UUID
for _, rId := range dto.Roles {
rid, err := convert.StringToUUID(rId)
if err == nil {
roleIdList = append(roleIdList, rid)
}
}
err = rRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
UserID: userUUID,
Column2: roleIdList,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to assign roles")
}
if err := tx.Commit(ctx); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
}
finalUser, err := u.userRepo.GetByID(ctx, userUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created user")
}
return finalUser.ToResponse(), nil
}
func (u *userService) ChangePassword(ctx context.Context, userId string, dto *request.ChangePasswordDto) *fiber.Error {
tx, err := u.db.Begin(ctx)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
@@ -66,11 +141,11 @@ func (u *userService) ChangePassword(ctx context.Context, userId string, dto *re
pgID, err := convert.StringToUUID(userId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
}
user, err := u.userRepo.GetByID(ctx, pgID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
return fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
}
if user == nil {
return fiber.NewError(fiber.StatusNotFound, "User not found")
@@ -81,15 +156,15 @@ func (u *userService) ChangePassword(ctx context.Context, userId string, dto *re
return fiber.NewError(fiber.StatusBadRequest, "Old password required")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.OldPassword)); err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid password!")
return fiber.NewError(fiber.StatusUnauthorized, "Invalid old password")
}
} else if user.PasswordHash == "" && dto.OldPassword != "" {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request")
return fiber.NewError(fiber.StatusBadRequest, "Invalid request: user has no password")
}
hashPassword, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "Failed to hash new password")
}
err = uRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
@@ -97,7 +172,7 @@ func (u *userService) ChangePassword(ctx context.Context, userId string, dto *re
PasswordHash: pgtype.Text{String: string(hashPassword), Valid: true},
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update password")
}
err = uRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
@@ -105,7 +180,7 @@ func (u *userService) ChangePassword(ctx context.Context, userId string, dto *re
TokenVersion: user.TokenVersion + 1,
})
if err != nil {
return err
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update token version")
}
if err := tx.Commit(ctx); err != nil {
@@ -114,7 +189,7 @@ func (u *userService) ChangePassword(ctx context.Context, userId string, dto *re
return nil
}
func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, error) {
func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, *fiber.Error) {
tx, err := u.db.Begin(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
@@ -126,12 +201,12 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
userUUID, err := convert.StringToUUID(userId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
}
user, err := u.userRepo.GetByID(ctx, userUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
return nil, fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
}
if user == nil {
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
@@ -139,7 +214,7 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
newListRole, err := u.roleRepo.GetByIDs(ctx, dto.Roles)
if err != nil {
return nil, err
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch roles")
}
hasUserRole := false
@@ -163,20 +238,20 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
}
if !hasUserRole {
return nil, fiber.NewError(fiber.StatusNotFound, "User must have the USER role")
return nil, fiber.NewError(fiber.StatusForbidden, "User must have the USER role")
}
if slices.Contains(claims.Roles, constants.RoleTypeMod) && !slices.Contains(claims.Roles, constants.RoleTypeAdmin) {
if hasAdminRole {
return nil, fiber.NewError(fiber.StatusForbidden, "MOD cannot assign ADMIN role to any user")
return nil, fiber.NewError(fiber.StatusForbidden, "MOD cannot assign ADMIN role")
}
if userId == claims.UId && !hasModRole {
return nil, fiber.NewError(fiber.StatusForbidden, "You can't remove MOD role of yourself")
return nil, fiber.NewError(fiber.StatusForbidden, "You cannot remove your own MOD role")
}
if userId == claims.UId && hasBannedRole {
return nil, fiber.NewError(fiber.StatusForbidden, "You can't assign BANNED role to yourself")
return nil, fiber.NewError(fiber.StatusForbidden, "You cannot ban yourself")
}
isTargetAdminOrMod := false
for _, r := range user.Roles {
@@ -186,17 +261,17 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
}
}
if isTargetAdminOrMod && hasBannedRole {
return nil, fiber.NewError(fiber.StatusForbidden, "MOD cannot assign BANNED role to an ADMIN or MOD user")
return nil, fiber.NewError(fiber.StatusForbidden, "MOD cannot ban an ADMIN or MOD")
}
}
if slices.Contains(claims.Roles, constants.RoleTypeAdmin) {
if userId == claims.UId && hasBannedRole {
return nil, fiber.NewError(fiber.StatusForbidden, "You can't assign BANNED role to yourself")
return nil, fiber.NewError(fiber.StatusForbidden, "You cannot ban yourself")
}
if userId == claims.UId && !hasAdminRole {
return nil, fiber.NewError(fiber.StatusForbidden, "You can't remove ADMIN role of yourself")
return nil, fiber.NewError(fiber.StatusForbidden, "You cannot remove your own ADMIN role")
}
}
@@ -213,7 +288,7 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
err = rRepo.BulkDeleteRolesFromUser(ctx, userUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to clear old roles")
}
err = rRepo.CreateUserRole(ctx, sqlc.CreateUserRoleParams{
@@ -221,7 +296,7 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
Column2: roleIdList,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create new roles")
}
err = uRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
@@ -229,13 +304,13 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
TokenVersion: user.TokenVersion + 1,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update token version")
}
user.TokenVersion += 1
err = tx.Commit(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
}
mapCache := map[string]any{
@@ -247,33 +322,33 @@ func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims
return user.ToResponse(), nil
}
func (u *userService) DeleteUser(ctx context.Context, userId string) error {
func (u *userService) DeleteUser(ctx context.Context, userId string) *fiber.Error {
pgID, err := convert.StringToUUID(userId)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
}
user, err := u.userRepo.GetByID(ctx, pgID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
return fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
}
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 fiber.NewError(fiber.StatusInternalServerError, "Failed to delete user")
}
return nil
}
func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, error) {
func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *request.UpdateProfileDto) (*response.UserResponse, *fiber.Error) {
pgID, err := convert.StringToUUID(userId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
}
user, err := u.userRepo.GetByID(ctx, pgID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
return nil, fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
}
if user == nil {
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
@@ -294,20 +369,20 @@ func (u *userService) UpdateProfile(ctx context.Context, userId string, dto *req
},
)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update profile")
}
return newUser.ToResponse(), nil
}
func (u *userService) RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error) {
func (u *userService) RestoreUser(ctx context.Context, userId string) (*response.UserResponse, *fiber.Error) {
pgID, err := convert.StringToUUID(userId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
}
user, err := u.userRepo.GetByIDWithoutDeleted(ctx, pgID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
return nil, fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
}
if user == nil {
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
@@ -315,7 +390,7 @@ func (u *userService) RestoreUser(ctx context.Context, userId string) (*response
err = u.userRepo.Restore(ctx, pgID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to restore user")
}
user.IsDeleted = false
return user.ToResponse(), nil
@@ -362,7 +437,7 @@ func (m *userService) fillSearchArgs(arg *sqlc.SearchUsersParams, dto *request.S
}
}
func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, *fiber.Error) {
if dto.Page < 1 {
dto.Page = 1
}
@@ -404,7 +479,7 @@ func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto
})
if err := g.Wait(); err != nil {
return nil, err
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search users")
}
users := models.UsersEntityToResponse(rows)
@@ -412,14 +487,17 @@ func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto
return response.BuildPaginatedResponse(users, totalRecords, dto.Page, dto.Limit), nil
}
func (u *userService) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error) {
func (u *userService) GetUserByID(ctx context.Context, userId string) (*response.UserResponse, *fiber.Error) {
pgID, err := convert.StringToUUID(userId)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
}
user, err := u.userRepo.GetByID(ctx, pgID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, err.Error())
return nil, fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
}
if user == nil {
return nil, fiber.NewError(fiber.StatusNotFound, "User not found")
}
return user.ToResponse(), nil
}