Refactor user module to support ClientID and enhance user management features

This commit is contained in:
Habib Fatkhul Rohman 2025-10-15 21:10:13 +07:00
parent cdd940463d
commit 2d20f892e7
6 changed files with 186 additions and 110 deletions

View File

@ -12,6 +12,7 @@ import (
"github.com/Caknoooo/go-pagination"
"github.com/gin-gonic/gin"
"github.com/samber/do"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@ -20,6 +21,7 @@ type (
Register(ctx *gin.Context)
Login(ctx *gin.Context)
Me(ctx *gin.Context)
GetUserById(ctx *gin.Context)
Refresh(ctx *gin.Context)
GetAllUser(ctx *gin.Context)
SendVerificationEmail(ctx *gin.Context)
@ -62,16 +64,18 @@ func (c *userController) Register(ctx *gin.Context) {
}
func (c *userController) GetAllUser(ctx *gin.Context) {
tenantId := ctx.MustGet("tenant_id").(string)
clientId := ctx.MustGet("client_id").(string)
logrus.Info("Client ID: ", clientId)
var filter = &query.UserFilter{
TenantID: tenantId,
Name: ctx.Query("name"), // ambil parameter name dari query string
ClientID: clientId,
Name: ctx.Query("name"), // example additional filter
}
logrus.Info("Filter: ", filter)
filter.BindPagination(ctx)
ctx.ShouldBindQuery(filter)
users, total, err := pagination.PaginatedQueryWithIncludable[query.User](c.db, filter)
users, total, err := pagination.PaginatedQueryWithIncludable[query.M_User](c.db, filter)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_USER, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
@ -97,6 +101,20 @@ func (c *userController) Me(ctx *gin.Context) {
ctx.JSON(http.StatusOK, res)
}
func (c *userController) GetUserById(ctx *gin.Context) {
userId := ctx.Param("id")
result, err := c.userService.GetUserById(ctx.Request.Context(), userId)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_USER, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_GET_USER, result)
ctx.JSON(http.StatusOK, res)
}
func (c *userController) Login(ctx *gin.Context) {
var req dto.UserLoginRequest
if err := ctx.ShouldBind(&req); err != nil {

View File

@ -2,10 +2,10 @@ package dto
import (
"errors"
"mime/multipart"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
"github.com/Caknoooo/go-gin-clean-starter/pkg/dto"
"github.com/google/uuid"
)
const (
@ -51,20 +51,28 @@ var (
type (
UserCreateRequest struct {
Name string `json:"name" form:"name" binding:"required,min=2,max=100"`
TelpNumber string `json:"telp_number" form:"telp_number" binding:"omitempty,min=8,max=20"`
Email string `json:"email" form:"email" binding:"required,email"`
Username string `json:"username" form:"username" binding:"required,min=2,max=100"`
Password string `json:"password" form:"password" binding:"required,min=8"`
Image *multipart.FileHeader `json:"image" form:"image"`
Gender string `json:"gender" form:"gender" binding:"omitempty,max=10"`
Address string `json:"address" form:"address" binding:"omitempty"`
Phone string `json:"phone" form:"phone" binding:"omitempty,min=8,max=20"`
Email string `json:"email" form:"email" binding:"required,email"`
PhotoUrl string `json:"photo_url" form:"photo_url" binding:"omitempty"`
ClientID uuid.UUID `json:"client_id" form:"client_id" binding:"required,uuid4"`
MaintenanceGroupUserID uuid.UUID `json:"maintenance_group_user_id" form:"maintenance_group_user_id" binding:"omitempty,uuid4"`
LocationID uuid.UUID `json:"location_id" form:"location_id" binding:"omitempty,uuid4"`
}
UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
Gender string `json:"gender"`
Address string `json:"address"`
Phone string `json:"phone"`
Email string `json:"email"`
TelpNumber string `json:"telp_number"`
Role string `json:"role"`
ImageUrl string `json:"image_url"`
IsVerified bool `json:"is_verified"`
PhotoUrl string `json:"photo_url"`
}
UserPaginationResponse struct {
@ -79,17 +87,27 @@ type (
UserUpdateRequest struct {
Name string `json:"name" form:"name" binding:"omitempty,min=2,max=100"`
TelpNumber string `json:"telp_number" form:"telp_number" binding:"omitempty,min=8,max=20"`
Username string `json:"username" form:"username" binding:"omitempty,min=2,max=100"`
Password string `json:"password" form:"password" binding:"omitempty,min=8"`
Gender string `json:"gender" form:"gender" binding:"omitempty,max=10"`
Address string `json:"address" form:"address" binding:"omitempty"`
Phone string `json:"phone" form:"phone" binding:"omitempty,min=8,max=20"`
Email string `json:"email" form:"email" binding:"omitempty,email"`
PhotoUrl string `json:"photo_url" form:"photo_url" binding:"omitempty"`
ClientID uuid.UUID `json:"client_id" form:"client_id" binding:"omitempty,uuid4"`
MaintenanceGroupUserID uuid.UUID `json:"maintenance_group_user_id" form:"maintenance_group_user_id" binding:"omitempty,uuid4"`
LocationID uuid.UUID `json:"location_id" form:"location_id" binding:"omitempty,uuid4"`
}
UserUpdateResponse struct {
ID string `json:"id"`
Name string `json:"name"`
TelpNumber string `json:"telp_number"`
Role string `json:"role"`
Username string `json:"username"`
Gender string `json:"gender"`
Address string `json:"address"`
Phone string `json:"phone"`
Email string `json:"email"`
IsVerified bool `json:"is_verified"`
PhotoUrl string `json:"photo_url"`
}
SendVerificationEmailRequest struct {

View File

@ -5,20 +5,22 @@ import (
"gorm.io/gorm"
)
type User struct {
type M_User struct {
ID string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
Gender string `json:"gender"`
Address string `json:"address"`
Phone string `json:"phone"`
Email string `json:"email"`
TelpNumber string `json:"telp_number"`
Role string `json:"role"`
ImageUrl string `json:"image_url"`
IsVerified bool `json:"is_verified"`
PhotoUrl string `json:"photo_url"`
}
type UserFilter struct {
pagination.BaseFilter
Name string `form:"name"` // tambahkan ini
TenantID string `form:"tenant_id"` // tambahkan ini
ClientID string `form:"client_id"` // tambahkan ini
}
func (f *UserFilter) ApplyFilters(query *gorm.DB) *gorm.DB {
@ -26,14 +28,14 @@ func (f *UserFilter) ApplyFilters(query *gorm.DB) *gorm.DB {
if f.Name != "" {
query = query.Where("name ILIKE ?", "%"+f.Name+"%")
}
if f.TenantID != "" {
query = query.Where("tenant_id = ?", f.TenantID)
if f.ClientID != "" {
query = query.Where("client_id = ?", f.ClientID)
}
return query
}
func (f *UserFilter) GetTableName() string {
return "users"
return "m_users"
}
func (f *UserFilter) GetSearchFields() []string {

View File

@ -9,11 +9,11 @@ import (
type (
UserRepository interface {
Register(ctx context.Context, tx *gorm.DB, user entities.User) (entities.User, error)
GetUserById(ctx context.Context, tx *gorm.DB, userId string) (entities.User, error)
GetUserByEmail(ctx context.Context, tx *gorm.DB, email string) (entities.User, error)
CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.User, bool, error)
Update(ctx context.Context, tx *gorm.DB, user entities.User) (entities.User, error)
Register(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error)
GetUserById(ctx context.Context, tx *gorm.DB, userId string) (entities.M_User, error)
GetUserByEmail(ctx context.Context, tx *gorm.DB, email string) (entities.M_User, error)
CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.M_User, bool, error)
Update(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error)
Delete(ctx context.Context, tx *gorm.DB, userId string) error
}
@ -28,64 +28,64 @@ func NewUserRepository(db *gorm.DB) UserRepository {
}
}
func (r *userRepository) Register(ctx context.Context, tx *gorm.DB, user entities.User) (entities.User, error) {
func (r *userRepository) Register(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Create(&user).Error; err != nil {
return entities.User{}, err
return entities.M_User{}, err
}
return user, nil
}
func (r *userRepository) GetUserById(ctx context.Context, tx *gorm.DB, userId string) (entities.User, error) {
func (r *userRepository) GetUserById(ctx context.Context, tx *gorm.DB, userId string) (entities.M_User, error) {
if tx == nil {
tx = r.db
}
var user entities.User
var user entities.M_User
if err := tx.WithContext(ctx).Where("id = ?", userId).Take(&user).Error; err != nil {
return entities.User{}, err
return entities.M_User{}, err
}
return user, nil
}
func (r *userRepository) GetUserByEmail(ctx context.Context, tx *gorm.DB, email string) (entities.User, error) {
func (r *userRepository) GetUserByEmail(ctx context.Context, tx *gorm.DB, email string) (entities.M_User, error) {
if tx == nil {
tx = r.db
}
var user entities.User
var user entities.M_User
if err := tx.WithContext(ctx).Where("email = ?", email).Take(&user).Error; err != nil {
return entities.User{}, err
return entities.M_User{}, err
}
return user, nil
}
func (r *userRepository) CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.User, bool, error) {
func (r *userRepository) CheckEmail(ctx context.Context, tx *gorm.DB, email string) (entities.M_User, bool, error) {
if tx == nil {
tx = r.db
}
var user entities.User
var user entities.M_User
if err := tx.WithContext(ctx).Where("email = ?", email).Take(&user).Error; err != nil {
return entities.User{}, false, err
return entities.M_User{}, false, err
}
return user, true, nil
}
func (r *userRepository) Update(ctx context.Context, tx *gorm.DB, user entities.User) (entities.User, error) {
func (r *userRepository) Update(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error) {
if tx == nil {
tx = r.db
}
if err := tx.WithContext(ctx).Updates(&user).Error; err != nil {
return entities.User{}, err
return entities.M_User{}, err
}
return user, nil
@ -96,7 +96,7 @@ func (r *userRepository) Delete(ctx context.Context, tx *gorm.DB, userId string)
tx = r.db
}
if err := tx.WithContext(ctx).Delete(&entities.User{}, "id = ?", userId).Error; err != nil {
if err := tx.WithContext(ctx).Delete(&entities.M_User{}, "id = ?", userId).Error; err != nil {
return err
}

View File

@ -13,16 +13,17 @@ func RegisterRoutes(server *gin.Engine, injector *do.Injector) {
userController := do.MustInvoke[controller.UserController](injector)
jwtService := do.MustInvokeNamed[service.JWTService](injector, constants.JWTService)
userRoutes := server.Group("/api/user")
userRoutes := server.Group("/api/v1/user")
{
userRoutes.POST("", userController.Register)
userRoutes.POST("/login", userController.Login)
// userRoutes.POST("", userController.Register)
// userRoutes.POST("/login", userController.Login)
// userRoutes.POST("/verify-email", userController.VerifyEmail)
userRoutes.GET("", middlewares.Authenticate(jwtService), userController.GetAllUser)
userRoutes.GET("/me", middlewares.Authenticate(jwtService), userController.Me)
userRoutes.GET("/:id", middlewares.Authenticate(jwtService), userController.GetUserById)
userRoutes.PUT("/:id", middlewares.Authenticate(jwtService), userController.Update)
userRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), userController.Delete)
userRoutes.POST("/send-verification-email", userController.SendVerificationEmail)
userRoutes.POST("/verify-email", userController.VerifyEmail)
userRoutes.POST("/refresh", middlewares.Authenticate(jwtService), userController.Refresh)
}
}

View File

@ -2,6 +2,7 @@ package service
import (
"context"
"fmt"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
authDto "github.com/Caknoooo/go-gin-clean-starter/modules/auth/dto"
@ -9,8 +10,6 @@ import (
authService "github.com/Caknoooo/go-gin-clean-starter/modules/auth/service"
"github.com/Caknoooo/go-gin-clean-starter/modules/user/dto"
"github.com/Caknoooo/go-gin-clean-starter/modules/user/repository"
"github.com/Caknoooo/go-gin-clean-starter/pkg/constants"
"github.com/Caknoooo/go-gin-clean-starter/pkg/helpers"
"github.com/Caknoooo/go-gin-clean-starter/pkg/utils"
"github.com/google/uuid"
"gorm.io/gorm"
@ -57,29 +56,36 @@ func (s *userService) Register(ctx context.Context, req dto.UserCreateRequest) (
return dto.UserResponse{}, dto.ErrEmailAlreadyExists
}
user := entities.User{
ID: uuid.New(),
enryptPassword, err := utils.HashPassword(req.Password)
if err != nil {
return dto.UserResponse{}, err
}
user := entities.M_User{
Name: req.Name,
Username: req.Username,
Email: req.Email,
TelpNumber: req.TelpNumber,
Password: req.Password,
Role: constants.ENUM_ROLE_USER,
IsVerified: false,
Password: enryptPassword,
Gender: req.Gender,
Address: req.Address,
Phone: req.Phone,
PhotoUrl: req.PhotoUrl,
ClientID: req.ClientID,
}
createdUser, err := s.userRepository.Register(ctx, s.db, user)
if err != nil {
return dto.UserResponse{}, err
}
return dto.UserResponse{
ID: createdUser.ID.String(),
Name: createdUser.Name,
Username: createdUser.Username,
Email: createdUser.Email,
TelpNumber: createdUser.TelpNumber,
Role: createdUser.Role,
ImageUrl: createdUser.ImageUrl,
IsVerified: createdUser.IsVerified,
Phone: createdUser.Phone,
Gender: createdUser.Gender,
Address: createdUser.Address,
PhotoUrl: createdUser.PhotoUrl,
}, nil
}
@ -92,11 +98,12 @@ func (s *userService) GetUserById(ctx context.Context, userId string) (dto.UserR
return dto.UserResponse{
ID: user.ID.String(),
Name: user.Name,
Username: user.Username,
Email: user.Email,
TelpNumber: user.TelpNumber,
Role: user.Role,
ImageUrl: user.ImageUrl,
IsVerified: user.IsVerified,
Gender: user.Gender,
Address: user.Address,
Phone: user.Phone,
PhotoUrl: user.PhotoUrl,
}, nil
}
@ -106,18 +113,20 @@ func (s *userService) Verify(ctx context.Context, req dto.UserLoginRequest) (aut
return authDto.TokenResponse{}, dto.ErrEmailNotFound
}
isValid, err := helpers.CheckPassword(user.Password, []byte(req.Password))
if err != nil || !isValid {
isValid := utils.CheckPasswordHash(req.Password, user.Password)
if !isValid {
fmt.Println("Password validation error:", err)
return authDto.TokenResponse{}, dto.ErrUserNotFound
}
accessToken := s.jwtService.GenerateAccessToken(user.TenantID.String(), user.ID.String(), user.Role)
accessToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String())
refreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken()
refreshToken := entities.RefreshToken{
ID: uuid.New(),
UserID: user.ID,
TenantID: user.TenantID,
// TenantID: user.TenantID,
ClientID: user.ClientID,
Token: refreshTokenString,
ExpiresAt: expiresAt,
}
@ -130,7 +139,6 @@ func (s *userService) Verify(ctx context.Context, req dto.UserLoginRequest) (aut
return authDto.TokenResponse{
AccessToken: accessToken,
RefreshToken: refreshTokenString,
Role: user.Role,
}, nil
}
@ -140,11 +148,11 @@ func (s *userService) SendVerificationEmail(ctx context.Context, req dto.SendVer
return dto.ErrEmailNotFound
}
if user.IsVerified {
return dto.ErrAccountAlreadyVerified
}
// if user.IsVerified {
// return dto.ErrAccountAlreadyVerified
// }
verificationToken := s.jwtService.GenerateAccessToken(user.TenantID.String(), user.ID.String(), "verification")
verificationToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String())
subject := "Email Verification"
body := "Please verify your email using this token: " + verificationToken
@ -168,7 +176,7 @@ func (s *userService) VerifyEmail(ctx context.Context, req dto.VerifyEmailReques
return dto.VerifyEmailResponse{}, dto.ErrUserNotFound
}
user.IsVerified = true
// user.IsVerified = true
updatedUser, err := s.userRepository.Update(ctx, s.db, user)
if err != nil {
return dto.VerifyEmailResponse{}, err
@ -176,7 +184,7 @@ func (s *userService) VerifyEmail(ctx context.Context, req dto.VerifyEmailReques
return dto.VerifyEmailResponse{
Email: updatedUser.Email,
IsVerified: updatedUser.IsVerified,
// IsVerified: updatedUser.IsVerified,
}, nil
}
@ -192,8 +200,36 @@ func (s *userService) Update(ctx context.Context, req dto.UserUpdateRequest, use
if req.Email != "" {
user.Email = req.Email
}
if req.TelpNumber != "" {
user.TelpNumber = req.TelpNumber
if req.Username != "" {
user.Username = req.Username
}
if req.Password != "" {
enryptPassword, err := utils.HashPassword(req.Password)
if err != nil {
return dto.UserUpdateResponse{}, err
}
user.Password = enryptPassword
}
if req.Gender != "" {
user.Gender = req.Gender
}
if req.Address != "" {
user.Address = req.Address
}
if req.Phone != "" {
user.Phone = req.Phone
}
if req.PhotoUrl != "" {
user.PhotoUrl = req.PhotoUrl
}
if req.ClientID != uuid.Nil {
user.ClientID = req.ClientID
}
if req.MaintenanceGroupUserID != uuid.Nil {
user.MaintenanceGroupUserID = req.MaintenanceGroupUserID
}
if req.LocationID != uuid.Nil {
user.LocationID = req.LocationID
}
updatedUser, err := s.userRepository.Update(ctx, s.db, user)
@ -204,10 +240,12 @@ func (s *userService) Update(ctx context.Context, req dto.UserUpdateRequest, use
return dto.UserUpdateResponse{
ID: updatedUser.ID.String(),
Name: updatedUser.Name,
TelpNumber: updatedUser.TelpNumber,
Role: updatedUser.Role,
Username: updatedUser.Username,
Phone: updatedUser.Phone,
Email: updatedUser.Email,
IsVerified: updatedUser.IsVerified,
Gender: updatedUser.Gender,
Address: updatedUser.Address,
PhotoUrl: updatedUser.PhotoUrl,
}, nil
}
@ -221,7 +259,7 @@ func (s *userService) RefreshToken(ctx context.Context, req authDto.RefreshToken
return authDto.TokenResponse{}, err
}
accessToken := s.jwtService.GenerateAccessToken(refreshToken.TenantID.String(), refreshToken.UserID.String(), refreshToken.User.Role)
accessToken := s.jwtService.GenerateAccessToken(refreshToken.ClientID.String(), refreshToken.UserID.String())
newRefreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken()
err = s.refreshTokenRepository.DeleteByToken(ctx, s.db, req.RefreshToken)
@ -244,6 +282,5 @@ func (s *userService) RefreshToken(ctx context.Context, req authDto.RefreshToken
return authDto.TokenResponse{
AccessToken: accessToken,
RefreshToken: newRefreshTokenString,
Role: refreshToken.User.Role,
}, nil
}