feat: add user warehouse management functionality with CRUD operations
Deploy Application / deploy (push) Successful in 24s Details

- Implemented MUserWarehouseEntity for user-warehouse relationships
- Updated user service and repository to handle warehouse assignments
- Added new routes and controller methods for managing user warehouses
- Enhanced JWT service to include warehouse information in tokens
- Updated user DTOs to support warehouse data
This commit is contained in:
Habib Fatkhul Rohman 2025-11-19 14:56:33 +07:00
parent 18832a0400
commit be0b54b4a5
10 changed files with 703 additions and 78 deletions

View File

@ -16,8 +16,11 @@ type M_User struct {
MaintenanceGroupUserID uuid.UUID `gorm:"type:uuid;index" json:"maintenance_group_user_id"`
LocationID uuid.UUID `gorm:"type:uuid;index" json:"location_id"`
Client M_Client `gorm:"foreignKey:ClientID;references:ID"`
UserRoles []M_User_Role `gorm:"foreignKey:UserID;references:ID" json:"user_roles"`
Roles []M_Role `gorm:"many2many:m_user_roles;foreignKey:ID;joinForeignKey:UserID;References:ID;JoinReferences:RoleID" json:"roles"`
Client M_Client `gorm:"foreignKey:ClientID;references:ID"`
UserRoles []M_User_Role `gorm:"foreignKey:UserID;references:ID" json:"user_roles"`
Roles []M_Role `gorm:"many2many:m_user_roles;foreignKey:ID;joinForeignKey:UserID;References:ID;JoinReferences:RoleID" json:"roles"`
UserWarehouses []MUserWarehouseEntity `gorm:"foreignKey:UserID;references:ID" json:"user_warehouses"`
Warehouses []MWarehouseEntity `gorm:"many2many:m_user_warehouses;foreignKey:ID;joinForeignKey:UserID;References:ID;JoinReferences:WarehouseID" json:"warehouses"`
FullAuditTrail
}

View File

@ -0,0 +1,18 @@
package entities
import "github.com/google/uuid"
type MUserWarehouseEntity struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:uuid_generate_v4()" json:"id"`
UserID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:idx_user_warehouses_user_id_warehouse_id" json:"user_id"`
WarehouseID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:idx_user_warehouses_user_id_warehouse_id" json:"warehouse_id"`
User M_User `gorm:"foreignKey:UserID;references:ID"`
Warehouse MWarehouseEntity `gorm:"foreignKey:WarehouseID;references:ID"`
Timestamp
}
func (MUserWarehouseEntity) TableName() string {
return "m_user_warehouses"
}

View File

@ -28,6 +28,7 @@ func Migrate(db *gorm.DB) error {
&entities.MWarehouseEntity{},
&entities.MZonaEntity{},
&entities.MAisleEntity{},
&entities.MUserWarehouseEntity{},
&entities.TAssignmentEntity{},
&entities.TAssignmentUserEntity{},
&entities.TInventoryReceiptEntity{},
@ -73,20 +74,21 @@ func MigrateFresh(db *gorm.DB) error {
// &entities.MWarehouseEntity{},
// &entities.MZonaEntity{},
// &entities.MAisleEntity{},
&entities.MUserWarehouseEntity{},
// &entities.TAssignmentEntity{},
// &entities.TAssignmentUserEntity{},
&entities.TInventoryReceiptEntity{},
&entities.TInventoryReceiptLineEntity{},
&entities.TInventoryRequestEntity{},
&entities.TInventoryRequestLineEntity{},
&entities.TInventoryIssueEntity{},
&entities.TInventoryIssueLineEntity{},
&entities.TInventoryReturnEntity{},
&entities.TInventoryReturnLineEntity{},
&entities.InventoryTransactionEntity{},
&entities.InventoryStorageEntity{},
&entities.TInventoryMovementEntity{},
&entities.TInventoryMovementLineEntity{},
// &entities.TInventoryReceiptEntity{},
// &entities.TInventoryReceiptLineEntity{},
// &entities.TInventoryRequestEntity{},
// &entities.TInventoryRequestLineEntity{},
// &entities.TInventoryIssueEntity{},
// &entities.TInventoryIssueLineEntity{},
// &entities.TInventoryReturnEntity{},
// &entities.TInventoryReturnLineEntity{},
// &entities.InventoryTransactionEntity{},
// &entities.InventoryStorageEntity{},
// &entities.TInventoryMovementEntity{},
// &entities.TInventoryMovementLineEntity{},
); err != nil {
return err
}

View File

@ -50,6 +50,8 @@ func Authenticate(jwtService service.JWTService) gin.HandlerFunc {
ctx.Set("token", authHeader)
ctx.Set("client_id", tokenInfo.ClientID)
ctx.Set("user_id", tokenInfo.UserID)
ctx.Set("role_id", tokenInfo.RoleID)
ctx.Set("warehouse_id", tokenInfo.WarehouseID)
ctx.Next()
}
}

View File

@ -12,22 +12,24 @@ import (
)
type UserTokenInfo struct {
ClientID string `json:"client_id"`
UserID string `json:"user_id"`
RoleID string `json:"role_id"`
ClientID string `json:"client_id"`
UserID string `json:"user_id"`
RoleID string `json:"role_id"`
WarehouseID string `json:"warehouse_id"`
}
type JWTService interface {
GenerateAccessToken(clientId string, userId string, roleId string) string
GenerateAccessToken(clientId string, userId string, roleId string, warehouseId string) string
GenerateRefreshToken() (string, time.Time)
ValidateToken(token string) (*jwt.Token, error)
GetUserIDByToken(token string) (*UserTokenInfo, error)
}
type jwtCustomClaim struct {
ClientID string `json:"client_id"`
UserID string `json:"user_id"`
RoleID string `json:"role_id"`
ClientID string `json:"client_id"`
UserID string `json:"user_id"`
RoleID string `json:"role_id"`
WarehouseID string `json:"warehouse_id"`
jwt.RegisteredClaims
}
@ -56,11 +58,12 @@ func getSecretKey() string {
return secretKey
}
func (j *jwtService) GenerateAccessToken(clientId string, userId string, roleId string) string {
func (j *jwtService) GenerateAccessToken(clientId string, userId string, roleId string, warehouseId string) string {
claims := jwtCustomClaim{
ClientID: clientId,
UserID: userId,
RoleID: roleId,
ClientID: clientId,
UserID: userId,
RoleID: roleId,
WarehouseID: warehouseId,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(j.accessExpiry)),
Issuer: j.issuer,
@ -111,10 +114,12 @@ func (j *jwtService) GetUserIDByToken(token string) (*UserTokenInfo, error) {
userId := fmt.Sprintf("%v", claims["user_id"])
clientId := fmt.Sprintf("%v", claims["client_id"])
roleId := fmt.Sprintf("%v", claims["role_id"])
warehouseId := fmt.Sprintf("%v", claims["warehouse_id"])
return &UserTokenInfo{
UserID: userId,
ClientID: clientId,
RoleID: roleId,
UserID: userId,
ClientID: clientId,
RoleID: roleId,
WarehouseID: warehouseId,
}, nil
}

View File

@ -15,6 +15,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/samber/do"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@ -22,6 +23,8 @@ type (
UserController interface {
Register(ctx *gin.Context)
Login(ctx *gin.Context)
SwitchRole(ctx *gin.Context)
SwitchWarehouse(ctx *gin.Context)
Me(ctx *gin.Context)
GetUserById(ctx *gin.Context)
Refresh(ctx *gin.Context)
@ -31,6 +34,10 @@ type (
Update(ctx *gin.Context)
Delete(ctx *gin.Context)
Create(ctx *gin.Context)
GetUserWarehouses(ctx *gin.Context)
AddUserWarehouse(ctx *gin.Context)
ReplaceUserWarehouses(ctx *gin.Context)
RemoveUserWarehouse(ctx *gin.Context)
}
userController struct {
@ -39,6 +46,115 @@ type (
}
)
// SwitchRole implements UserController.
func (c *userController) SwitchRole(ctx *gin.Context) {
userId := ctx.Param("id")
var req dto.SwitchRoleRequest
if err := ctx.ShouldBind(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.AbortWithStatusJSON(http.StatusBadRequest, res)
return
}
result, err := c.userService.SwitchRole(ctx.Request.Context(), userId, req)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_PROSES_REQUEST, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_PROSES_REQUEST, result)
ctx.JSON(http.StatusOK, res)
}
// SwitchWarehouse implements UserController.
func (c *userController) SwitchWarehouse(ctx *gin.Context) {
userId := ctx.Param("id")
var req dto.SwitchWarehouseRequest
if err := ctx.ShouldBind(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.AbortWithStatusJSON(http.StatusBadRequest, res)
return
}
result, err := c.userService.SwitchWarehouse(ctx.Request.Context(), userId, req)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_PROSES_REQUEST, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_PROSES_REQUEST, result)
ctx.JSON(http.StatusOK, res)
}
// AddUserWarehouse implements UserController.
func (c *userController) AddUserWarehouse(ctx *gin.Context) {
userId := ctx.Param("id")
var req dto.AddUserWarehouseRequest
if err := ctx.ShouldBind(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.AbortWithStatusJSON(http.StatusBadRequest, res)
return
}
err := c.userService.AssignWarehouse(ctx.Request.Context(), userId, req.WarehouseID)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_PROSES_REQUEST, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_PROSES_REQUEST, nil)
ctx.JSON(http.StatusOK, res)
}
// GetUserWarehouses implements UserController.
func (c *userController) GetUserWarehouses(ctx *gin.Context) {
userId := ctx.Param("id")
logrus.Infof("Getting warehouses for user ID: %s", userId)
result, err := c.userService.GetUserWarehouses(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)
}
// RemoveUserWarehouse implements UserController.
func (c *userController) RemoveUserWarehouse(ctx *gin.Context) {
userId := ctx.Param("id")
var req dto.RemoveUserWarehouseRequest
if err := ctx.ShouldBind(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.AbortWithStatusJSON(http.StatusBadRequest, res)
return
}
err := c.userService.RemoveWarehouse(ctx.Request.Context(), userId, req.WarehouseID)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_PROSES_REQUEST, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_PROSES_REQUEST, nil)
ctx.JSON(http.StatusOK, res)
}
// ReplaceUserWarehouses implements UserController.
func (c *userController) ReplaceUserWarehouses(ctx *gin.Context) {
userId := ctx.Param("id")
var req dto.ReplaceUserWarehousesRequest
if err := ctx.ShouldBind(&req); err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_GET_DATA_FROM_BODY, err.Error(), nil)
ctx.AbortWithStatusJSON(http.StatusBadRequest, res)
return
}
err := c.userService.AssignWarehousesReplace(ctx.Request.Context(), userId, req.WarehouseIDs)
if err != nil {
res := utils.BuildResponseFailed(dto.MESSAGE_FAILED_PROSES_REQUEST, err.Error(), nil)
ctx.JSON(http.StatusBadRequest, res)
return
}
res := utils.BuildResponseSuccess(dto.MESSAGE_SUCCESS_PROSES_REQUEST, nil)
ctx.JSON(http.StatusOK, res)
}
// Create implements UserController.
func (c *userController) Create(ctx *gin.Context) {
var req dto.UserCreateRequest

View File

@ -32,6 +32,7 @@ const (
MESSAGE_SUCCESS_DELETE_USER = "success delete user"
MESSAGE_SEND_VERIFICATION_EMAIL_SUCCESS = "success send verification email"
MESSAGE_SUCCESS_VERIFY_EMAIL = "success verify email"
MESSAGE_SUCCESS_PROSES_REQUEST = "success proses request"
)
var (
@ -64,17 +65,18 @@ type (
}
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"`
PhotoUrl string `json:"photo_url"`
Roles []UserRolesResponse `json:"roles,omitempty"`
Client dto.IdNameResponse `json:"client"`
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"`
PhotoUrl string `json:"photo_url"`
Roles []UserRolesResponse `json:"roles,omitempty"`
Client dto.IdNameResponse `json:"client"`
Warehouses []dto.IdNameResponse `json:"warehouses"`
}
UserRolesResponse struct {
@ -136,6 +138,24 @@ type (
}
SwitchRoleRequest struct {
RoleID string `json:"role_id" form:"role_id" binding:"required,uuid4"`
RoleID string `json:"role_id" form:"role_id" binding:"required,uuid4"`
ActiveWarehouseID string `json:"active_warehouse_id" form:"active_warehouse_id" binding:"required,uuid4"`
}
SwitchWarehouseRequest struct {
ActiveRoleID string `json:"active_role_id" form:"active_role_id" binding:"required,uuid4"`
WarehouseID string `json:"warehouse_id" form:"warehouse_id" binding:"required,uuid4"`
}
AddUserWarehouseRequest struct {
WarehouseID string `json:"warehouse_id" form:"warehouse_id" binding:"required,uuid4"`
}
RemoveUserWarehouseRequest struct {
WarehouseID string `json:"warehouse_id" form:"warehouse_id" binding:"required,uuid4"`
}
ReplaceUserWarehousesRequest struct {
WarehouseIDs []string `json:"warehouse_ids" form:"warehouse_ids" binding:"required,dive,uuid4"`
}
)

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/Caknoooo/go-gin-clean-starter/database/entities"
"github.com/google/uuid"
"gorm.io/gorm"
)
@ -19,6 +20,14 @@ type (
Delete(ctx context.Context, tx *gorm.DB, userId string) error
SwitchRole(ctx context.Context, tx *gorm.DB, userId string, roleId string) (entities.M_User, error)
GetAll(ctx context.Context, tx *gorm.DB) ([]entities.M_User, error)
GetUserWarehouses(ctx context.Context, tx *gorm.DB, userId string) ([]entities.MWarehouseEntity, error)
SwitchWarehouse(ctx context.Context, tx *gorm.DB, userId string, warehouseId string) (entities.M_User, error)
AssignWarehousesToUser(ctx context.Context, tx *gorm.DB, userId string, warehouseIds []string) (entities.M_User, error)
RemoveWarehousesFromUser(ctx context.Context, tx *gorm.DB, userId string, warehouseIds []string) (entities.M_User, error)
ClearUserWarehouses(ctx context.Context, tx *gorm.DB, userId string) error
AddUserWarehouses(ctx context.Context, tx *gorm.DB, userId string, warehouseIds []string) error
AddUserWarehouse(ctx context.Context, tx *gorm.DB, userId string, warehouseId string) error
RemoveUserWarehouse(ctx context.Context, tx *gorm.DB, userId string, warehouseId string) error
}
userRepository struct {
@ -26,6 +35,219 @@ type (
}
)
// GetUserWarehouses implements UserRepository.
func (r *userRepository) GetUserWarehouses(ctx context.Context, tx *gorm.DB, userId string) ([]entities.MWarehouseEntity, error) {
if tx == nil {
tx = r.db
}
var user entities.M_User
// Preload relasi UserWarehouses dan Warehouse
if err := tx.WithContext(ctx).
Preload("UserWarehouses.Warehouse").
Where("id = ?", userId).
Take(&user).Error; err != nil {
return nil, err
}
var warehouses []entities.MWarehouseEntity
for _, uw := range user.UserWarehouses {
warehouses = append(warehouses, uw.Warehouse)
}
return warehouses, nil
}
// AddUserWarehouse implements UserRepository.
func (r *userRepository) AddUserWarehouse(ctx context.Context, tx *gorm.DB, userId string, warehouseId string) error {
if tx == nil {
tx = r.db
}
uid := uuid.MustParse(userId)
wid := uuid.MustParse(warehouseId)
// Hapus dulu relasi yang sama jika ada
if err := tx.WithContext(ctx).
Where("user_id = ? AND warehouse_id = ?", uid, wid).
Delete(&entities.MUserWarehouseEntity{}).Error; err != nil {
return err
}
// Insert relasi baru
entity := entities.MUserWarehouseEntity{
ID: uuid.New(),
UserID: uid,
WarehouseID: wid,
}
if err := tx.WithContext(ctx).Create(&entity).Error; err != nil {
return err
}
return nil
}
// RemoveUserWarehouse implements UserRepository.
func (r *userRepository) RemoveUserWarehouse(ctx context.Context, tx *gorm.DB, userId string, warehouseId string) error {
if tx == nil {
tx = r.db
}
// Delete specific warehouse relation
if err := tx.WithContext(ctx).
Where("user_id = ? AND warehouse_id = ?", userId, warehouseId).
Delete(&entities.MUserWarehouseEntity{}).Error; err != nil {
return err
}
return nil
}
func (r *userRepository) ClearUserWarehouses(ctx context.Context, tx *gorm.DB, userId string) error {
return tx.WithContext(ctx).
Where("user_id = ?", userId).
Delete(&entities.MUserWarehouseEntity{}).Error
}
func (r *userRepository) AddUserWarehouses(ctx context.Context, tx *gorm.DB, userId string, warehouseIds []string) error {
assignments := make([]entities.MUserWarehouseEntity, 0, len(warehouseIds))
uid := uuid.MustParse(userId)
for _, wid := range warehouseIds {
assignments = append(assignments, entities.MUserWarehouseEntity{
ID: uuid.New(),
UserID: uid,
WarehouseID: uuid.MustParse(wid),
})
}
return tx.WithContext(ctx).Create(&assignments).Error
}
// AssignWarehousesToUser implements UserRepository.
func (r *userRepository) AssignWarehousesToUser(ctx context.Context, tx *gorm.DB, userId string, warehouseIds []string) (entities.M_User, error) {
if tx == nil {
tx = r.db
}
// Remove duplicates
warehouseMap := make(map[string]struct{})
for _, wid := range warehouseIds {
warehouseMap[wid] = struct{}{}
}
var uniqueWarehouseIds []string
for wid := range warehouseMap {
uniqueWarehouseIds = append(uniqueWarehouseIds, wid)
}
// Prepare assignments
var assignments []entities.MUserWarehouseEntity
for _, wid := range uniqueWarehouseIds {
assignments = append(assignments, entities.MUserWarehouseEntity{
ID: uuid.New(),
UserID: uuid.MustParse(userId),
WarehouseID: uuid.MustParse(wid),
})
}
// Insert assignments, ignore duplicates due to unique index
if err := tx.WithContext(ctx).Create(&assignments).Error; err != nil {
return entities.M_User{}, err
}
// Reload user with updated warehouses
var user entities.M_User
if err := tx.WithContext(ctx).
Preload("Client").
Preload("Roles").
Preload("Warehouses").
Where("id = ?", userId).
Take(&user).Error; err != nil {
return entities.M_User{}, err
}
return user, nil
}
// RemoveWarehousesFromUser implements UserRepository.
func (r *userRepository) RemoveWarehousesFromUser(ctx context.Context, tx *gorm.DB, userId string, warehouseIds []string) (entities.M_User, error) {
if tx == nil {
tx = r.db
}
// Remove assignments
if err := tx.WithContext(ctx).
Where("user_id = ? AND warehouse_id IN ?", userId, warehouseIds).
Delete(&entities.MUserWarehouseEntity{}).Error; err != nil {
return entities.M_User{}, err
}
// Reload user with updated warehouses
var user entities.M_User
if err := tx.WithContext(ctx).
Preload("Client").
Preload("Roles").
Preload("Warehouses").
Where("id = ?", userId).
Take(&user).Error; err != nil {
return entities.M_User{}, err
}
return user, nil
}
// SwitchWarehouse implements UserRepository.
func (r *userRepository) SwitchWarehouse(ctx context.Context, tx *gorm.DB, userId string, warehouseId string) (entities.M_User, error) {
if tx == nil {
tx = r.db
}
var user entities.M_User
// Get user with warehouses
if err := tx.WithContext(ctx).
Preload("Warehouses").
Where("id = ?", userId).
Take(&user).Error; err != nil {
return entities.M_User{}, err
}
var warehouse entities.MWarehouseEntity
if err := tx.WithContext(ctx).
Where("id = ?", warehouseId).
Take(&warehouse).Error; err != nil {
return entities.M_User{}, err
}
// Remove all existing warehouse relations for this user
if err := tx.WithContext(ctx).
Where("user_id = ?", userId).
Delete(&entities.MUserWarehouseEntity{}).Error; err != nil {
return entities.M_User{}, err
}
// Add new warehouse relation
newUserWarehouse := entities.MUserWarehouseEntity{
UserID: user.ID,
WarehouseID: warehouse.ID,
}
if err := tx.WithContext(ctx).Create(&newUserWarehouse).Error; err != nil {
return entities.M_User{}, err
}
// Reload user with updated warehouses
if err := tx.WithContext(ctx).
Preload("Warehouses").
Where("id = ?", userId).
Take(&user).Error; err != nil {
return entities.M_User{}, err
}
return user, nil
}
// GetAll implements UserRepository.
func (r *userRepository) GetAll(ctx context.Context, tx *gorm.DB) ([]entities.M_User, error) {
if tx == nil {
@ -66,6 +288,8 @@ func (r *userRepository) GetUserByUsername(ctx context.Context, tx *gorm.DB, use
var user entities.M_User
if err := tx.WithContext(ctx).
Preload("Roles").
Preload("Client").
Preload("Warehouses").
Where("username = ?", username).
Take(&user).Error; err != nil {
return entities.M_User{}, err
@ -74,12 +298,6 @@ func (r *userRepository) GetUserByUsername(ctx context.Context, tx *gorm.DB, use
return user, nil
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{
db: db,
}
}
// SwitchRole implements UserRepository.
func (r *userRepository) SwitchRole(ctx context.Context, tx *gorm.DB, userId string, roleId string) (entities.M_User, error) {
if tx == nil {
@ -120,29 +338,6 @@ func (r *userRepository) SwitchRole(ctx context.Context, tx *gorm.DB, userId str
return user, nil
}
// func (r *userRepository) SwitchRole(ctx context.Context, tx *gorm.DB, userId string, roleId string) (entities.M_User, error) {
// if tx == nil {
// tx = r.db
// }
// var user entities.M_User
// if err := tx.WithContext(ctx).Where("id = ?", userId).Take(&user).Error; err != nil {
// return entities.M_User{}, err
// }
// // Update field ActiveRoleID
// user.ActiveRoleID = roleId
// if err := tx.WithContext(ctx).Save(&user).Error; err != nil {
// return entities.M_User{}, err
// }
// // Preload roles jika perlu
// if err := tx.WithContext(ctx).
// Where("id = ?", userId).
// Preload("UserRoles.Role").
// Take(&user).Error; err != nil {
// return entities.M_User{}, err
// }
// return user, nil
// }
func (r *userRepository) Register(ctx context.Context, tx *gorm.DB, user entities.M_User) (entities.M_User, error) {
if tx == nil {
tx = r.db
@ -164,6 +359,7 @@ func (r *userRepository) GetUserById(ctx context.Context, tx *gorm.DB, userId st
if err := tx.WithContext(ctx).
Preload("Client").
Preload("Roles").
Preload("Warehouses").
Where("id = ?", userId).
Take(&user).Error; err != nil {
return entities.M_User{}, err
@ -179,7 +375,9 @@ func (r *userRepository) GetUserByEmail(ctx context.Context, tx *gorm.DB, email
var user entities.M_User
if err := tx.WithContext(ctx).
Preload("Client").
Preload("Roles").
Preload("Warehouses").
Where("email = ?", email).
Take(&user).Error; err != nil {
return entities.M_User{}, err
@ -224,3 +422,9 @@ func (r *userRepository) Delete(ctx context.Context, tx *gorm.DB, userId string)
return nil
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{
db: db,
}
}

View File

@ -25,5 +25,15 @@ func RegisterRoutes(server *gin.Engine, injector *do.Injector) {
userRoutes.DELETE("/:id", middlewares.Authenticate(jwtService), userController.Delete)
userRoutes.POST("/send-verification-email", userController.SendVerificationEmail)
userRoutes.POST("/refresh", middlewares.Authenticate(jwtService), userController.Refresh)
// Switch role & warehouse
userRoutes.POST("/:id/switch-role", middlewares.Authenticate(jwtService), userController.SwitchRole)
userRoutes.POST("/:id/switch-warehouse", middlewares.Authenticate(jwtService), userController.SwitchWarehouse)
// user warehouse routes
userRoutes.GET("/:id/warehouses", middlewares.Authenticate(jwtService), userController.GetUserWarehouses)
userRoutes.POST("/:id/warehouses", middlewares.Authenticate(jwtService), userController.AddUserWarehouse)
userRoutes.PUT("/:id/warehouses", middlewares.Authenticate(jwtService), userController.ReplaceUserWarehouses)
userRoutes.DELETE("/:id/warehouses/:warehouseId", middlewares.Authenticate(jwtService), userController.RemoveUserWarehouse)
}
}

View File

@ -10,8 +10,10 @@ import (
authDto "github.com/Caknoooo/go-gin-clean-starter/modules/auth/dto"
authRepo "github.com/Caknoooo/go-gin-clean-starter/modules/auth/repository"
authService "github.com/Caknoooo/go-gin-clean-starter/modules/auth/service"
rolerepository "github.com/Caknoooo/go-gin-clean-starter/modules/role/repository"
"github.com/Caknoooo/go-gin-clean-starter/modules/user/dto"
"github.com/Caknoooo/go-gin-clean-starter/modules/user/repository"
warehouserepository "github.com/Caknoooo/go-gin-clean-starter/modules/warehouse/repository"
pkgdto "github.com/Caknoooo/go-gin-clean-starter/pkg/dto"
"github.com/Caknoooo/go-gin-clean-starter/pkg/utils"
"github.com/google/uuid"
@ -28,17 +30,191 @@ type UserService interface {
Update(ctx context.Context, req dto.UserUpdateRequest, userId string) (dto.UserUpdateResponse, error)
Delete(ctx context.Context, userId string) error
RefreshToken(ctx context.Context, req authDto.RefreshTokenRequest) (authDto.TokenResponse, error)
SwitchRole(ctx context.Context, req dto.SwitchRoleRequest) (authDto.TokenResponse, error)
SwitchRole(ctx context.Context, userId string, req dto.SwitchRoleRequest) (authDto.TokenResponse, error)
GetAll(ctx context.Context) ([]dto.UserResponse, error)
GetUserWarehouses(ctx context.Context, userId string) ([]dto.UserResponse, error)
SwitchWarehouse(ctx context.Context, userId string, req dto.SwitchWarehouseRequest) (authDto.TokenResponse, error)
// AssignWarehouseToUser(ctx context.Context, userId string, warehouseIds []string) error
// RemoveWarehouseFromUser(ctx context.Context, userId string, warehouseIds []string) error
// ClearUserWarehouses(ctx context.Context, userId string) error
AssignWarehousesReplace(ctx context.Context, userId string, warehouseIds []string) error
AssignWarehouse(ctx context.Context, userId string, warehouseId string) error
RemoveWarehouse(ctx context.Context, userId string, warehouseId string) error
}
type userService struct {
userRepository repository.UserRepository
roleRepository rolerepository.RoleRepository
warehouserepository warehouserepository.WarehouseRepository
refreshTokenRepository authRepo.RefreshTokenRepository
jwtService authService.JWTService
db *gorm.DB
}
// GetUserWarehouses implements UserService.
func (s *userService) GetUserWarehouses(ctx context.Context, userId string) ([]dto.UserResponse, error) {
user, err := s.userRepository.GetUserById(ctx, s.db, userId)
if err != nil {
return nil, err
}
var warehouses []pkgdto.IdNameResponse
for _, uw := range user.Warehouses {
warehouses = append(warehouses, pkgdto.IdNameResponse{
ID: uw.ID.String(),
Name: uw.Name,
})
}
response := dto.UserResponse{
ID: user.ID.String(),
Name: user.Name,
Warehouses: warehouses,
}
return []dto.UserResponse{response}, nil
}
// // AssignWarehouseToUser implements UserService.
// func (s *userService) AssignWarehouseToUser(ctx context.Context, userId string, warehouseIds []string) error {
// // Validasi userId
// if _, err := uuid.Parse(userId); err != nil {
// return errors.New("invalid userId format")
// }
// // Validasi warehouseIds
// for _, wid := range warehouseIds {
// if _, err := uuid.Parse(wid); err != nil {
// return errors.New("invalid warehouseId format: " + wid)
// }
// }
// // Hapus semua relasi warehouse lama user, lalu assign yang baru (replace)
// _, err := s.userRepository.AssignWarehousesToUser(ctx, s.db, userId, warehouseIds)
// if err != nil {
// return err
// }
// return nil
// }
// // RemoveWarehouseFromUser implements UserService.
// func (s *userService) RemoveWarehouseFromUser(ctx context.Context, userId string, warehouseIds []string) error {
// // Validasi userId
// if _, err := uuid.Parse(userId); err != nil {
// return errors.New("invalid userId format")
// }
// // Validasi warehouseIds
// for _, wid := range warehouseIds {
// if _, err := uuid.Parse(wid); err != nil {
// return errors.New("invalid warehouseId format: " + wid)
// }
// }
// _, err := s.userRepository.RemoveWarehousesFromUser(ctx, s.db, userId, warehouseIds)
// if err != nil {
// return err
// }
// return nil
// }
// SwitchWarehouse implements UserService.
func (s *userService) SwitchWarehouse(ctx context.Context, userId string, req dto.SwitchWarehouseRequest) (authDto.TokenResponse, error) {
user, err := s.userRepository.GetUserById(ctx, s.db, userId)
if err != nil {
return authDto.TokenResponse{}, err
}
// Delete old refresh tokens for user
err = s.refreshTokenRepository.DeleteByUserID(ctx, s.db, user.ID.String())
if err != nil {
return authDto.TokenResponse{}, err
}
// Get first role and warehouse for token generation
if len(user.Roles) == 0 {
return authDto.TokenResponse{}, errors.New("user has no roles assigned")
}
if len(user.Warehouses) == 0 {
return authDto.TokenResponse{}, errors.New("user has no warehouses assigned")
}
role, err := s.roleRepository.GetRoleByID(ctx, s.db, req.ActiveRoleID)
if err != nil {
return authDto.TokenResponse{}, err
}
warehouse, err := s.warehouserepository.GetById(ctx, s.db, req.WarehouseID)
if err != nil {
return authDto.TokenResponse{}, err
}
accessToken := s.jwtService.GenerateAccessToken(
user.ClientID.String(),
userId,
role.ID.String(), // gunakan role yang sedang aktif
warehouse.ID.String(), // warehouse yang dipilih
)
refreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken()
refreshToken := entities.RefreshToken{
ID: uuid.New(),
UserID: user.ID,
ClientID: user.ClientID,
Token: refreshTokenString,
ExpiresAt: expiresAt,
}
_, err = s.refreshTokenRepository.Create(ctx, s.db, refreshToken)
if err != nil {
return authDto.TokenResponse{}, err
}
return authDto.TokenResponse{
AccessToken: accessToken,
RefreshToken: refreshTokenString,
}, nil
}
func (s *userService) AssignWarehouse(ctx context.Context, userId string, warehouseId string) error {
return s.userRepository.AddUserWarehouse(ctx, s.db, userId, warehouseId)
}
func (s *userService) RemoveWarehouse(ctx context.Context, userId string, warehouseId string) error {
return s.userRepository.RemoveUserWarehouse(ctx, s.db, userId, warehouseId)
}
func (s *userService) AssignWarehousesReplace(ctx context.Context, userId string, warehouseIds []string) error {
// Validasi input
if _, err := uuid.Parse(userId); err != nil {
return errors.New("invalid userId format")
}
for _, wid := range warehouseIds {
if _, err := uuid.Parse(wid); err != nil {
return fmt.Errorf("invalid warehouseId format: %s", wid)
}
}
// Mulai transaksi
tx := s.db.Begin()
// Hapus relasi lama
if err := s.userRepository.ClearUserWarehouses(ctx, tx, userId); err != nil {
tx.Rollback()
return err
}
// Tambahkan relasi baru
if err := s.userRepository.AddUserWarehouses(ctx, tx, userId, warehouseIds); err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
}
// GetAll implements UserService.
func (s *userService) GetAll(ctx context.Context) ([]dto.UserResponse, error) {
users, err := s.userRepository.GetAll(ctx, s.db)
@ -130,12 +306,16 @@ func (s *userService) Create(ctx context.Context, req dto.UserCreateRequest) (dt
func NewUserService(
userRepo repository.UserRepository,
roleRepo rolerepository.RoleRepository,
warehouserepository warehouserepository.WarehouseRepository,
refreshTokenRepo authRepo.RefreshTokenRepository,
jwtService authService.JWTService,
db *gorm.DB,
) UserService {
return &userService{
userRepository: userRepo,
roleRepository: roleRepo,
warehouserepository: warehouserepository,
refreshTokenRepository: refreshTokenRepo,
jwtService: jwtService,
db: db,
@ -143,8 +323,60 @@ func NewUserService(
}
// SwitchRole implements UserService.
func (s *userService) SwitchRole(ctx context.Context, req dto.SwitchRoleRequest) (authDto.TokenResponse, error) {
panic("unimplemented")
func (s *userService) SwitchRole(ctx context.Context, userId string, req dto.SwitchRoleRequest) (authDto.TokenResponse, error) {
// Switch user role in repository
user, err := s.userRepository.GetUserById(ctx, s.db, userId)
if err != nil {
return authDto.TokenResponse{}, err
}
// Delete old refresh tokens for user
err = s.refreshTokenRepository.DeleteByUserID(ctx, s.db, user.ID.String())
if err != nil {
return authDto.TokenResponse{}, err
}
// Get first role and warehouse for token generation
if len(user.Roles) == 0 {
return authDto.TokenResponse{}, errors.New("user has no roles assigned")
}
if len(user.Warehouses) == 0 {
return authDto.TokenResponse{}, errors.New("user has no warehouses assigned")
}
role, err := s.roleRepository.GetRoleByID(ctx, s.db, req.RoleID)
if err != nil {
return authDto.TokenResponse{}, err
}
warehouse, err := s.warehouserepository.GetById(ctx, s.db, req.ActiveWarehouseID)
if err != nil {
return authDto.TokenResponse{}, err
}
accessToken := s.jwtService.GenerateAccessToken(
user.ClientID.String(),
userId,
role.ID.String(), // gunakan role yang sedang aktif
warehouse.ID.String(), // warehouse yang dipilih
)
refreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken()
refreshToken := entities.RefreshToken{
ID: uuid.New(),
UserID: user.ID,
ClientID: user.ClientID,
Token: refreshTokenString,
ExpiresAt: expiresAt,
}
_, err = s.refreshTokenRepository.Create(ctx, s.db, refreshToken)
if err != nil {
return authDto.TokenResponse{}, err
}
return authDto.TokenResponse{
AccessToken: accessToken,
RefreshToken: refreshTokenString,
}, nil
}
func (s *userService) Register(ctx context.Context, req dto.UserCreateRequest) (dto.UserResponse, error) {
@ -243,18 +475,22 @@ func (s *userService) Verify(ctx context.Context, req dto.UserLoginRequest) (aut
return authDto.TokenResponse{}, dto.ErrUserNotFound
}
roles := append([]entities.M_Role{}, user.Roles...)
if len(roles) == 0 {
return authDto.TokenResponse{}, errors.New("user has no roles assigned")
}
warehouses := append([]entities.MWarehouseEntity{}, user.Warehouses...)
if len(warehouses) == 0 {
return authDto.TokenResponse{}, errors.New("user has no warehouses assigned")
}
// DeleteByUserID
err = s.refreshTokenRepository.DeleteByUserID(ctx, s.db, user.ID.String())
if err != nil {
return authDto.TokenResponse{}, err
}
accessToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String(), roles[0].ID.String())
accessToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String(), roles[0].ID.String(), warehouses[0].ID.String())
refreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken()
refreshToken := entities.RefreshToken{
@ -291,7 +527,12 @@ func (s *userService) SendVerificationEmail(ctx context.Context, req dto.SendVer
return errors.New("user has no roles assigned")
}
verificationToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String(), roles[0].ID.String())
warehouses := append([]entities.MWarehouseEntity{}, user.Warehouses...)
if len(warehouses) == 0 {
return errors.New("user has no warehouses assigned")
}
verificationToken := s.jwtService.GenerateAccessToken(user.ClientID.String(), user.ID.String(), roles[0].ID.String(), warehouses[0].ID.String())
subject := "Email Verification"
body := "Please verify your email using this token: " + verificationToken
@ -399,10 +640,14 @@ func (s *userService) RefreshToken(ctx context.Context, req authDto.RefreshToken
}
roles := append([]entities.M_Role{}, refreshToken.User.Roles...)
if len(roles) == 0 {
return authDto.TokenResponse{}, errors.New("1 user has no roles assigned")
return authDto.TokenResponse{}, errors.New("user has no roles assigned")
}
warehouses := append([]entities.MWarehouseEntity{}, refreshToken.User.Warehouses...)
if len(warehouses) == 0 {
return authDto.TokenResponse{}, errors.New("user has no warehouses assigned")
}
accessToken := s.jwtService.GenerateAccessToken(refreshToken.ClientID.String(), refreshToken.UserID.String(), roles[0].ID.String())
accessToken := s.jwtService.GenerateAccessToken(refreshToken.ClientID.String(), refreshToken.UserID.String(), roles[0].ID.String(), warehouses[0].ID.String())
// newRefreshTokenString, expiresAt := s.jwtService.GenerateRefreshToken()
// err = s.refreshTokenRepository.DeleteByToken(ctx, s.db, req.RefreshToken)