feat: add menu & action module, update fixing, etc

This commit is contained in:
hanif salafi 2026-01-19 02:58:10 +07:00
parent a98517f4be
commit bcb6326877
48 changed files with 8086 additions and 52 deletions

View File

@ -0,0 +1,27 @@
package entity
import (
"github.com/google/uuid"
"time"
)
// MenuActions menyimpan actions yang tersedia di setiap menu
// Contoh: Menu "Content Management" punya actions: view, create, edit, delete, approve, export
type MenuActions struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
MenuId uint `json:"menu_id" gorm:"type:int4;not null"`
ActionCode string `json:"action_code" gorm:"type:varchar(50);not null"` // 'view', 'create', 'edit', 'delete', 'approve', 'export'
ActionName string `json:"action_name" gorm:"type:varchar(255);not null"` // 'View Content', 'Create Content', etc.
Description *string `json:"description" gorm:"type:text"`
PathUrl *string `json:"path_url" gorm:"type:varchar(255)"` // Optional: untuk routing frontend
HttpMethod *string `json:"http_method" gorm:"type:varchar(10)"` // Optional: 'GET', 'POST', 'PUT', 'DELETE'
Position *int `json:"position" gorm:"type:int4"`
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// Relations
Menu *MasterMenus `json:"menu,omitempty" gorm:"foreignKey:MenuId"`
}

View File

@ -0,0 +1,24 @@
package entity
import (
"github.com/google/uuid"
"time"
)
// UserLevelMenuAccesses mengatur akses user_level ke menu tertentu
// Contoh: UserLevel "Admin Pusat" bisa akses semua menu, "Editor" hanya bisa akses "Content Management"
type UserLevelMenuAccesses struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserLevelId uint `json:"user_level_id" gorm:"type:int4;not null"`
MenuId uint `json:"menu_id" gorm:"type:int4;not null"`
CanAccess bool `json:"can_access" gorm:"type:bool;default:true"` // Apakah boleh akses menu ini
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// Relations
UserLevel *UserLevels `json:"user_level,omitempty" gorm:"foreignKey:UserLevelId"`
Menu *MasterMenus `json:"menu,omitempty" gorm:"foreignKey:MenuId"`
}

View File

@ -0,0 +1,25 @@
package entity
import (
"github.com/google/uuid"
"time"
)
// UserLevelMenuActionAccesses mengatur akses user_level ke action tertentu di dalam menu
// Contoh: UserLevel "Creator" di menu "Content Management" hanya bisa create dan edit, tidak bisa delete
type UserLevelMenuActionAccesses struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserLevelId uint `json:"user_level_id" gorm:"type:int4;not null"`
MenuId uint `json:"menu_id" gorm:"type:int4;not null"`
ActionCode string `json:"action_code" gorm:"type:varchar(50);not null"` // 'view', 'create', 'edit', 'delete', etc.
CanAccess bool `json:"can_access" gorm:"type:bool;default:true"` // Apakah boleh melakukan action ini
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
// Relations
UserLevel *UserLevels `json:"user_level,omitempty" gorm:"foreignKey:UserLevelId"`
Menu *MasterMenus `json:"menu,omitempty" gorm:"foreignKey:MenuId"`
}

View File

@ -127,6 +127,9 @@ func Models() []interface{} {
entity.MagazineFiles{},
entity.MasterMenus{},
entity.MasterModules{},
entity.MenuActions{}, // New: Menu actions (view, create, edit, delete, etc)
entity.UserLevelMenuAccesses{}, // New: User level menu access control
entity.UserLevelMenuActionAccesses{}, // New: User level menu action access control
entity.MasterStatuses{},
entity.MasterApprovalStatuses{},
entity.Provinces{},

View File

@ -0,0 +1,169 @@
package middleware
import (
"netidhub-saas-be/app/database"
"netidhub-saas-be/app/database/entity"
"github.com/gofiber/fiber/v2"
)
type MenuActionAccessMiddleware struct {
DB *database.Database
}
func NewMenuActionAccessMiddleware(db *database.Database) *MenuActionAccessMiddleware {
return &MenuActionAccessMiddleware{
DB: db,
}
}
// CheckMenuAccess middleware untuk validasi akses user_level ke menu tertentu
func (m *MenuActionAccessMiddleware) CheckMenuAccess(menuId uint) fiber.Handler {
return func(c *fiber.Ctx) error {
// Get user from context
userCtx := c.Locals("user")
if userCtx == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"success": false,
"code": 401,
"messages": []string{"User tidak terautentikasi"},
})
}
user, ok := userCtx.(*entity.Users)
if !ok || user == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"success": false,
"code": 401,
"messages": []string{"User tidak valid"},
})
}
// Get user role untuk mendapatkan user_level_id
var userRole entity.UserRoles
if err := m.DB.DB.Where("id = ?", user.UserRoleId).First(&userRole).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"code": 500,
"messages": []string{"Error mendapatkan user role"},
"error": err.Error(),
})
}
userLevelId := userRole.UserLevelId
// Check akses user_level ke menu
var access entity.UserLevelMenuAccesses
err := m.DB.DB.Where(
"user_level_id = ? AND menu_id = ? AND is_active = ? AND can_access = ?",
userLevelId,
menuId,
true,
true,
).First(&access).Error
if err != nil {
// Jika tidak ada record, berarti tidak ada akses
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"success": false,
"code": 403,
"messages": []string{"Anda tidak memiliki akses ke menu ini"},
"user_level_id": userLevelId,
"menu_id": menuId,
})
}
// Set menu ke context
c.Locals("menu_id", menuId)
c.Locals("user_level_id", userLevelId)
return c.Next()
}
}
// CheckMenuActionAccess middleware untuk validasi akses user_level ke action tertentu di dalam menu
func (m *MenuActionAccessMiddleware) CheckMenuActionAccess(menuId uint, actionCode string) fiber.Handler {
return func(c *fiber.Ctx) error {
// Get user from context
userCtx := c.Locals("user")
if userCtx == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"success": false,
"code": 401,
"messages": []string{"User tidak terautentikasi"},
})
}
user, ok := userCtx.(*entity.Users)
if !ok || user == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"success": false,
"code": 401,
"messages": []string{"User tidak valid"},
})
}
// Get user role untuk mendapatkan user_level_id
var userRole entity.UserRoles
if err := m.DB.DB.Where("id = ?", user.UserRoleId).First(&userRole).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"code": 500,
"messages": []string{"Error mendapatkan user role"},
"error": err.Error(),
})
}
userLevelId := userRole.UserLevelId
// First, check if user has access to the menu
var menuAccess entity.UserLevelMenuAccesses
err := m.DB.DB.Where(
"user_level_id = ? AND menu_id = ? AND is_active = ? AND can_access = ?",
userLevelId,
menuId,
true,
true,
).First(&menuAccess).Error
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"success": false,
"code": 403,
"messages": []string{"Anda tidak memiliki akses ke menu ini"},
"user_level_id": userLevelId,
"menu_id": menuId,
})
}
// Then, check if user has access to the specific action
var actionAccess entity.UserLevelMenuActionAccesses
err = m.DB.DB.Where(
"user_level_id = ? AND menu_id = ? AND action_code = ? AND is_active = ? AND can_access = ?",
userLevelId,
menuId,
actionCode,
true,
true,
).First(&actionAccess).Error
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"success": false,
"code": 403,
"messages": []string{"Anda tidak memiliki akses untuk melakukan action ini"},
"user_level_id": userLevelId,
"menu_id": menuId,
"action_code": actionCode,
})
}
// Set to context
c.Locals("menu_id", menuId)
c.Locals("action_code", actionCode)
c.Locals("user_level_id", userLevelId)
return c.Next()
}
}

View File

@ -13,6 +13,7 @@ func MasterMenusResponseMapper(masterMenusReq *entity.MasterMenus) (masterMenusR
Description: masterMenusReq.Description,
ModuleId: masterMenusReq.ModuleId,
ParentMenuId: masterMenusReq.ParentMenuId,
Group: masterMenusReq.Group,
Icon: masterMenusReq.Icon,
Position: masterMenusReq.Position,
StatusId: masterMenusReq.StatusId,

View File

@ -7,6 +7,8 @@ import (
"netidhub-saas-be/app/module/master_menus/request"
"netidhub-saas-be/utils/paginator"
"strings"
"gorm.io/gorm"
)
type masterMenusRepository struct {
@ -87,6 +89,10 @@ func (_i *masterMenusRepository) FindOne(id uint) (masterMenus *entity.MasterMen
func (_i *masterMenusRepository) FindLastMenuPosition() (position *int, err error) {
var masterMenus *entity.MasterMenus
if err := _i.DB.DB.Where("position IS NOT NULL").Order(fmt.Sprintf("%s %s", "position", "DESC")).First(&masterMenus).Error; err != nil {
// If no record found, return nil without error (it's expected when no menus exist yet)
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}

View File

@ -23,18 +23,22 @@ type MasterMenusQueryRequest struct {
type MasterMenusCreateRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
ModuleId int `json:"moduleId" validate:"required"`
ModuleId *int `json:"moduleId,omitempty"`
Group string `json:"group" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
ParentMenuId *int `json:"parentMenuId"`
Icon *string `json:"icon"`
ParentMenuId *int `json:"parentMenuId,omitempty"`
Icon *string `json:"icon,omitempty"`
}
func (req MasterMenusCreateRequest) ToEntity() *entity.MasterMenus {
moduleId := 0
if req.ModuleId != nil {
moduleId = *req.ModuleId
}
return &entity.MasterMenus{
Name: req.Name,
Description: req.Description,
ModuleId: req.ModuleId,
ModuleId: moduleId,
ParentMenuId: req.ParentMenuId,
Icon: req.Icon,
Group: req.Group,
@ -46,20 +50,24 @@ type MasterMenusUpdateRequest struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
ModuleId int `json:"moduleId" validate:"required"`
ModuleId *int `json:"moduleId,omitempty"`
Group string `json:"group" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
ParentMenuId *int `json:"parentMenuId"`
Icon *string `json:"icon"`
ParentMenuId *int `json:"parentMenuId,omitempty"`
Icon *string `json:"icon,omitempty"`
UpdatedAt time.Time `json:"updatedAt"`
}
func (req MasterMenusUpdateRequest) ToEntity() *entity.MasterMenus {
moduleId := 0
if req.ModuleId != nil {
moduleId = *req.ModuleId
}
return &entity.MasterMenus{
ID: req.ID,
Name: req.Name,
Description: req.Description,
ModuleId: req.ModuleId,
ModuleId: moduleId,
ParentMenuId: req.ParentMenuId,
Icon: req.Icon,
Group: req.Group,

View File

@ -8,6 +8,7 @@ type MasterMenusResponse struct {
Description string `json:"description"`
ModuleId int `json:"module_id"`
ParentMenuId *int `json:"parent_menu_id"`
Group string `json:"group"`
Icon *string `json:"icon"`
Position *int `json:"position"`
StatusId int `json:"status_id"`

View File

@ -60,14 +60,19 @@ func (_i *masterMenusService) Save(req request.MasterMenusCreateRequest) (err er
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
var latestPosition, _ = _i.Repo.FindLastMenuPosition()
latestPosition, err := _i.Repo.FindLastMenuPosition()
if err != nil {
return err
}
*latestPosition = *latestPosition + 1
if latestPosition != nil {
newReq.Position = latestPosition
// If no menu with position exists, start with position 1
position := 1
newReq.Position = &position
} else if latestPosition != nil {
// Increment the latest position
newPosition := *latestPosition + 1
newReq.Position = &newPosition
} else {
// Fallback: set position to 1
position := 1
newReq.Position = &position
}
return _i.Repo.Create(newReq)

View File

@ -12,6 +12,7 @@ func MasterModulesResponseMapper(masterModulesReq *entity.MasterModules) (master
Name: masterModulesReq.Name,
Description: masterModulesReq.Description,
PathUrl: masterModulesReq.PathUrl,
ActionType: masterModulesReq.ActionType,
StatusId: masterModulesReq.StatusId,
IsActive: masterModulesReq.IsActive,
CreatedAt: masterModulesReq.CreatedAt,

View File

@ -19,10 +19,12 @@ type MasterModulesQueryRequest struct {
}
type MasterModulesCreateRequest struct {
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
PathUrl string `json:"pathUrl" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
PathUrl string `json:"pathUrl" validate:"required"`
ActionType *string `json:"actionType"`
StatusId int `json:"statusId" validate:"required"`
MenuIds []int `json:"menuIds"` // Optional: untuk langsung assign ke menu saat create
}
func (req MasterModulesCreateRequest) ToEntity() *entity.MasterModules {
@ -30,16 +32,19 @@ func (req MasterModulesCreateRequest) ToEntity() *entity.MasterModules {
Name: req.Name,
Description: req.Description,
PathUrl: req.PathUrl,
ActionType: req.ActionType,
StatusId: req.StatusId,
}
}
type MasterModulesUpdateRequest struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
PathUrl string `json:"pathUrl" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Description string `json:"description" validate:"required"`
PathUrl string `json:"pathUrl" validate:"required"`
ActionType *string `json:"actionType"`
StatusId int `json:"statusId" validate:"required"`
MenuIds []int `json:"menuIds"` // Optional: untuk update relasi dengan menu
}
func (req MasterModulesUpdateRequest) ToEntity() *entity.MasterModules {
@ -48,6 +53,7 @@ func (req MasterModulesUpdateRequest) ToEntity() *entity.MasterModules {
Name: req.Name,
Description: req.Description,
PathUrl: req.PathUrl,
ActionType: req.ActionType,
StatusId: req.StatusId,
UpdatedAt: time.Now(),
}

View File

@ -7,6 +7,7 @@ type MasterModulesResponse struct {
Name string `json:"name"`
Description string `json:"description"`
PathUrl string `json:"path_url"`
ActionType *string `json:"action_type"`
StatusId int `json:"status_id"`
IsActive *bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`

View File

@ -0,0 +1,16 @@
package controller
import (
"netidhub-saas-be/app/module/menu_actions/service"
)
type Controller struct {
MenuActions MenuActionsController
}
func NewController(menuActionsService service.MenuActionsService) *Controller {
return &Controller{
MenuActions: NewMenuActionsController(menuActionsService),
}
}

View File

@ -0,0 +1,260 @@
package controller
import (
"netidhub-saas-be/app/module/menu_actions/request"
"netidhub-saas-be/app/module/menu_actions/service"
"netidhub-saas-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
utilRes "netidhub-saas-be/utils/response"
utilVal "netidhub-saas-be/utils/validator"
)
type menuActionsController struct {
menuActionsService service.MenuActionsService
}
type MenuActionsController interface {
All(c *fiber.Ctx) error
GetByMenuId(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
SaveBatch(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
}
func NewMenuActionsController(menuActionsService service.MenuActionsService) MenuActionsController {
return &menuActionsController{
menuActionsService: menuActionsService,
}
}
// All MenuActions
// @Summary Get all MenuActions
// @Description API for getting all MenuActions
// @Tags MenuActions
// @Security Bearer
// @Param menu_id query int false "Menu ID"
// @Param action_code query string false "Action Code"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /menu-actions [get]
func (_i *menuActionsController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
req := request.MenuActionsQueryRequest{
Pagination: paginate,
}
if menuId := c.Query("menu_id"); menuId != "" {
id, _ := strconv.ParseUint(menuId, 10, 0)
menuIdUint := uint(id)
req.MenuId = &menuIdUint
}
if actionCode := c.Query("action_code"); actionCode != "" {
req.ActionCode = &actionCode
}
menuActionsData, paging, err := _i.menuActionsService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuActions list successfully retrieved"},
Data: menuActionsData,
Meta: paging,
})
}
// GetByMenuId get MenuActions by Menu ID
// @Summary Get MenuActions by Menu ID
// @Description API for getting MenuActions by Menu ID
// @Tags MenuActions
// @Security Bearer
// @Param menu_id path int true "Menu ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /menu-actions/menu/{menu_id} [get]
func (_i *menuActionsController) GetByMenuId(c *fiber.Ctx) error {
menuId, err := strconv.ParseUint(c.Params("menu_id"), 10, 0)
if err != nil {
return err
}
menuActionsData, err := _i.menuActionsService.GetByMenuId(uint(menuId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuActions by menu successfully retrieved"},
Data: menuActionsData,
})
}
// Show get one MenuAction
// @Summary Get one MenuAction
// @Description API for getting one MenuAction
// @Tags MenuActions
// @Security Bearer
// @Param id path int true "MenuAction ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /menu-actions/{id} [get]
func (_i *menuActionsController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
menuActionData, err := _i.menuActionsService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuAction successfully retrieved"},
Data: menuActionData,
})
}
// Save create MenuAction
// @Summary Create MenuAction
// @Description API for create MenuAction
// @Tags MenuActions
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.MenuActionsCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /menu-actions [post]
func (_i *menuActionsController) Save(c *fiber.Ctx) error {
req := new(request.MenuActionsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.menuActionsService.Save(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuAction successfully created"},
})
}
// SaveBatch create MenuActions batch
// @Summary Create MenuActions batch
// @Description API for create MenuActions batch
// @Tags MenuActions
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.MenuActionsBatchCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /menu-actions/batch [post]
func (_i *menuActionsController) SaveBatch(c *fiber.Ctx) error {
req := new(request.MenuActionsBatchCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.menuActionsService.SaveBatch(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuActions batch successfully created"},
})
}
// Update MenuAction
// @Summary Update MenuAction
// @Description API for update MenuAction
// @Tags MenuActions
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param id path int true "MenuAction ID"
// @Param payload body request.MenuActionsUpdateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /menu-actions/{id} [put]
func (_i *menuActionsController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.MenuActionsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.menuActionsService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuAction successfully updated"},
})
}
// Delete MenuAction
// @Summary Delete MenuAction
// @Description API for delete MenuAction
// @Tags MenuActions
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param id path int true "MenuAction ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /menu-actions/{id} [delete]
func (_i *menuActionsController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.menuActionsService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuAction successfully deleted"},
})
}

View File

@ -0,0 +1,26 @@
package mapper
import (
"netidhub-saas-be/app/database/entity"
res "netidhub-saas-be/app/module/menu_actions/response"
)
func MenuActionsResponseMapper(menuActionReq *entity.MenuActions) (menuActionRes *res.MenuActionsResponse) {
if menuActionReq != nil {
menuActionRes = &res.MenuActionsResponse{
ID: menuActionReq.ID,
MenuId: menuActionReq.MenuId,
ActionCode: menuActionReq.ActionCode,
ActionName: menuActionReq.ActionName,
Description: menuActionReq.Description,
PathUrl: menuActionReq.PathUrl,
HttpMethod: menuActionReq.HttpMethod,
Position: menuActionReq.Position,
IsActive: menuActionReq.IsActive,
CreatedAt: menuActionReq.CreatedAt,
UpdatedAt: menuActionReq.UpdatedAt,
}
}
return menuActionRes
}

View File

@ -0,0 +1,56 @@
package menu_actions
import (
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
"netidhub-saas-be/app/module/menu_actions/controller"
"netidhub-saas-be/app/module/menu_actions/repository"
"netidhub-saas-be/app/module/menu_actions/service"
)
// struct of MenuActionsRouter
type MenuActionsRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of MenuActions module
var NewMenuActionsModule = fx.Options(
// register repository of MenuActions module
fx.Provide(repository.NewMenuActionsRepository),
// register service of MenuActions module
fx.Provide(service.NewMenuActionsService),
// register controller of MenuActions module
fx.Provide(controller.NewController),
// register router of MenuActions module
fx.Provide(NewMenuActionsRouter),
)
// init MenuActionsRouter
func NewMenuActionsRouter(fiber *fiber.App, controller *controller.Controller) *MenuActionsRouter {
return &MenuActionsRouter{
App: fiber,
Controller: controller,
}
}
// register routes of MenuActions module
func (_i *MenuActionsRouter) RegisterMenuActionsRoutes() {
// define controllers
menuActionsController := _i.Controller.MenuActions
// define routes
_i.App.Route("/menu-actions", func(router fiber.Router) {
router.Get("/", menuActionsController.All)
router.Get("/:id", menuActionsController.Show)
router.Get("/menu/:menu_id", menuActionsController.GetByMenuId)
router.Post("/", menuActionsController.Save)
router.Post("/batch", menuActionsController.SaveBatch)
router.Put("/:id", menuActionsController.Update)
router.Delete("/:id", menuActionsController.Delete)
})
}

View File

@ -0,0 +1,140 @@
package repository
import (
"fmt"
"netidhub-saas-be/app/database"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/menu_actions/request"
"netidhub-saas-be/utils/paginator"
)
type menuActionsRepository struct {
DB *database.Database
}
// MenuActionsRepository define interface of IMenuActionsRepository
type MenuActionsRepository interface {
GetAll(req request.MenuActionsQueryRequest) (menuActions []*entity.MenuActions, paging paginator.Pagination, err error)
GetByMenuId(menuId uint) (menuActions []*entity.MenuActions, err error)
GetByActionCode(menuId uint, actionCode string) (menuAction *entity.MenuActions, err error)
FindOne(id uint) (menuAction *entity.MenuActions, err error)
Create(menuAction *entity.MenuActions) (err error)
CreateBatch(menuActions []*entity.MenuActions) (err error)
Update(id uint, menuAction *entity.MenuActions) (err error)
Delete(id uint) (err error)
DeleteByMenuId(menuId uint) (err error)
}
func NewMenuActionsRepository(db *database.Database) MenuActionsRepository {
return &menuActionsRepository{
DB: db,
}
}
// implement interface of IMenuActionsRepository
func (_i *menuActionsRepository) GetAll(req request.MenuActionsQueryRequest) (menuActions []*entity.MenuActions, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.MenuActions{})
query = query.Where("is_active = ?", true)
if req.MenuId != nil {
query = query.Where("menu_id = ?", req.MenuId)
}
if req.ActionCode != nil && *req.ActionCode != "" {
query = query.Where("action_code = ?", req.ActionCode)
}
if req.ClientId != nil {
query = query.Where("client_id = ?", req.ClientId)
}
// Preload relations
query = query.Preload("Menu")
query.Count(&count)
// Apply sorting
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
} else {
query.Order("position ASC")
}
// Apply pagination (manual calculation like articles)
page := req.Pagination.Page
limit := req.Pagination.Limit
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 10
}
offset := (page - 1) * limit
err = query.Offset(offset).Limit(limit).Find(&menuActions).Error
if err != nil {
return
}
// Create pagination response
paging = paginator.Pagination{
Page: page,
Limit: limit,
Count: count,
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
}
return
}
func (_i *menuActionsRepository) GetByMenuId(menuId uint) (menuActions []*entity.MenuActions, err error) {
query := _i.DB.DB.Model(&entity.MenuActions{})
query = query.Where("menu_id = ? AND is_active = ?", menuId, true)
query = query.Preload("Menu")
query = query.Order("position ASC")
err = query.Find(&menuActions).Error
return
}
func (_i *menuActionsRepository) GetByActionCode(menuId uint, actionCode string) (menuAction *entity.MenuActions, err error) {
query := _i.DB.DB.Model(&entity.MenuActions{})
query = query.Where("menu_id = ? AND action_code = ? AND is_active = ?", menuId, actionCode, true)
query = query.Preload("Menu")
err = query.First(&menuAction).Error
return
}
func (_i *menuActionsRepository) FindOne(id uint) (menuAction *entity.MenuActions, err error) {
query := _i.DB.DB.Preload("Menu")
if err := query.First(&menuAction, id).Error; err != nil {
return nil, err
}
return menuAction, nil
}
func (_i *menuActionsRepository) Create(menuAction *entity.MenuActions) (err error) {
return _i.DB.DB.Create(menuAction).Error
}
func (_i *menuActionsRepository) CreateBatch(menuActions []*entity.MenuActions) (err error) {
return _i.DB.DB.Create(&menuActions).Error
}
func (_i *menuActionsRepository) Update(id uint, menuAction *entity.MenuActions) (err error) {
return _i.DB.DB.Model(&entity.MenuActions{}).
Where(&entity.MenuActions{ID: id}).
Updates(menuAction).Error
}
func (_i *menuActionsRepository) Delete(id uint) (err error) {
return _i.DB.DB.Delete(&entity.MenuActions{}, id).Error
}
func (_i *menuActionsRepository) DeleteByMenuId(menuId uint) (err error) {
return _i.DB.DB.Where("menu_id = ?", menuId).Delete(&entity.MenuActions{}).Error
}

View File

@ -0,0 +1,73 @@
package request
import (
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/utils/paginator"
"github.com/google/uuid"
)
type MenuActionsQueryRequest struct {
MenuId *uint `query:"menu_id"`
ActionCode *string `query:"action_code"`
ClientId *uuid.UUID `query:"client_id"`
Pagination *paginator.Pagination `query:"pagination"`
}
type MenuActionsCreateRequest struct {
MenuId uint `json:"menuId" validate:"required"`
ActionCode string `json:"actionCode" validate:"required"`
ActionName string `json:"actionName" validate:"required"`
Description *string `json:"description"`
PathUrl *string `json:"pathUrl"`
HttpMethod *string `json:"httpMethod"`
Position *int `json:"position"`
ClientId *uuid.UUID `json:"clientId"`
IsActive *bool `json:"isActive"`
}
func (req MenuActionsCreateRequest) ToEntity() *entity.MenuActions {
return &entity.MenuActions{
MenuId: req.MenuId,
ActionCode: req.ActionCode,
ActionName: req.ActionName,
Description: req.Description,
PathUrl: req.PathUrl,
HttpMethod: req.HttpMethod,
Position: req.Position,
ClientId: req.ClientId,
IsActive: req.IsActive,
}
}
type MenuActionsUpdateRequest struct {
MenuId uint `json:"menuId"`
ActionCode string `json:"actionCode"`
ActionName string `json:"actionName" validate:"required"`
Description *string `json:"description"`
PathUrl *string `json:"pathUrl"`
HttpMethod *string `json:"httpMethod"`
Position *int `json:"position"`
ClientId *uuid.UUID `json:"clientId"`
IsActive *bool `json:"isActive"`
}
func (req MenuActionsUpdateRequest) ToEntity() *entity.MenuActions {
return &entity.MenuActions{
MenuId: req.MenuId,
ActionCode: req.ActionCode,
ActionName: req.ActionName,
Description: req.Description,
PathUrl: req.PathUrl,
HttpMethod: req.HttpMethod,
Position: req.Position,
ClientId: req.ClientId,
IsActive: req.IsActive,
}
}
type MenuActionsBatchCreateRequest struct {
MenuId uint `json:"menuId" validate:"required"`
ActionCodes []string `json:"actionCodes" validate:"required,min=1"`
ClientId *uuid.UUID `json:"clientId"`
}

View File

@ -0,0 +1,18 @@
package response
import "time"
type MenuActionsResponse struct {
ID uint `json:"id"`
MenuId uint `json:"menuId"`
ActionCode string `json:"actionCode"`
ActionName string `json:"actionName"`
Description *string `json:"description"`
PathUrl *string `json:"pathUrl"`
HttpMethod *string `json:"httpMethod"`
Position *int `json:"position"`
IsActive *bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -0,0 +1,158 @@
package service
import (
"github.com/rs/zerolog"
"netidhub-saas-be/app/module/menu_actions/mapper"
"netidhub-saas-be/app/module/menu_actions/repository"
"netidhub-saas-be/app/module/menu_actions/request"
"netidhub-saas-be/app/module/menu_actions/response"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/utils/paginator"
)
// MenuActionsService
type menuActionsService struct {
Repo repository.MenuActionsRepository
Log zerolog.Logger
}
// MenuActionsService define interface of IMenuActionsService
type MenuActionsService interface {
All(req request.MenuActionsQueryRequest) (menuActions []*response.MenuActionsResponse, paging paginator.Pagination, err error)
GetByMenuId(menuId uint) (menuActions []*response.MenuActionsResponse, err error)
Show(id uint) (menuAction *response.MenuActionsResponse, err error)
Save(req request.MenuActionsCreateRequest) (err error)
SaveBatch(req request.MenuActionsBatchCreateRequest) (err error)
Update(id uint, req request.MenuActionsUpdateRequest) (err error)
Delete(id uint) error
}
// NewMenuActionsService init MenuActionsService
func NewMenuActionsService(repo repository.MenuActionsRepository, log zerolog.Logger) MenuActionsService {
return &menuActionsService{
Repo: repo,
Log: log,
}
}
// All implement interface of MenuActionsService
func (_i *menuActionsService) All(req request.MenuActionsQueryRequest) (menuActions []*response.MenuActionsResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
for _, result := range results {
menuActions = append(menuActions, mapper.MenuActionsResponseMapper(result))
}
return
}
func (_i *menuActionsService) GetByMenuId(menuId uint) (menuActions []*response.MenuActionsResponse, err error) {
results, err := _i.Repo.GetByMenuId(menuId)
if err != nil {
return nil, err
}
for _, result := range results {
menuActions = append(menuActions, mapper.MenuActionsResponseMapper(result))
}
return
}
func (_i *menuActionsService) Show(id uint) (menuAction *response.MenuActionsResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
return mapper.MenuActionsResponseMapper(result), nil
}
func (_i *menuActionsService) Save(req request.MenuActionsCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Creating menu action")
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
menuAction := req.ToEntity()
menuAction.IsActive = &isActive
return _i.Repo.Create(menuAction)
}
func (_i *menuActionsService) SaveBatch(req request.MenuActionsBatchCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Creating menu actions batch")
isActive := true
var menuActions []*entity.MenuActions
// Standard action names mapping
actionNameMap := map[string]string{
"view": "View",
"create": "Create",
"edit": "Edit",
"update": "Update",
"delete": "Delete",
"approve": "Approve",
"reject": "Reject",
"export": "Export",
"import": "Import",
"print": "Print",
}
for idx, actionCode := range req.ActionCodes {
position := idx + 1
actionName := actionNameMap[actionCode]
if actionName == "" {
// Capitalize first letter if not in map
if len(actionCode) > 0 {
actionName = string(actionCode[0]-32) + actionCode[1:]
} else {
actionName = actionCode
}
}
menuAction := &entity.MenuActions{
MenuId: req.MenuId,
ActionCode: actionCode,
ActionName: actionName,
Position: &position,
ClientId: req.ClientId,
IsActive: &isActive,
}
menuActions = append(menuActions, menuAction)
}
return _i.Repo.CreateBatch(menuActions)
}
func (_i *menuActionsService) Update(id uint, req request.MenuActionsUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Updating menu action")
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
menuAction := req.ToEntity()
menuAction.IsActive = &isActive
return _i.Repo.Update(id, menuAction)
}
func (_i *menuActionsService) Delete(id uint) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
isActive := false
result.IsActive = &isActive
return _i.Repo.Update(id, result)
}

View File

@ -54,6 +54,7 @@ func (_i *menuModulesRepository) GetAll(req request.MenuModulesQueryRequest) (me
query.Count(&count)
// Apply sorting
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
@ -64,15 +65,29 @@ func (_i *menuModulesRepository) GetAll(req request.MenuModulesQueryRequest) (me
query.Order("position ASC")
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
// Apply pagination (manual calculation like articles)
page := req.Pagination.Page
limit := req.Pagination.Limit
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 10
}
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&menuModules).Error
offset := (page - 1) * limit
err = query.Offset(offset).Limit(limit).Find(&menuModules).Error
if err != nil {
return
}
paging = *req.Pagination
// Create pagination response
paging = paginator.Pagination{
Page: page,
Limit: limit,
Count: count,
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
}
return
}

View File

@ -1,15 +1,16 @@
package request
import (
"github.com/google/uuid"
"netidhub-saas-be/utils/paginator"
"github.com/google/uuid"
)
type MenuModulesQueryRequest struct {
MenuId *uint `query:"menu_id"`
ModuleId *uint `query:"module_id"`
ClientId *uuid.UUID `query:"client_id"`
Pagination *paginator.Query `query:"pagination"`
MenuId *uint `query:"menu_id"`
ModuleId *uint `query:"module_id"`
ClientId *uuid.UUID `query:"client_id"`
Pagination *paginator.Pagination `query:"pagination"`
}
type MenuModulesCreateRequest struct {
@ -33,4 +34,3 @@ type MenuModulesUpdateRequest struct {
ClientId *uuid.UUID `json:"client_id"`
IsActive *bool `json:"is_active"`
}

View File

@ -0,0 +1,16 @@
package controller
import (
"netidhub-saas-be/app/module/user_level_menu_accesses/service"
)
type Controller struct {
UserLevelMenuAccesses UserLevelMenuAccessesController
}
func NewController(userLevelMenuAccessesService service.UserLevelMenuAccessesService) *Controller {
return &Controller{
UserLevelMenuAccesses: NewUserLevelMenuAccessesController(userLevelMenuAccessesService),
}
}

View File

@ -0,0 +1,328 @@
package controller
import (
"netidhub-saas-be/app/module/user_level_menu_accesses/request"
"netidhub-saas-be/app/module/user_level_menu_accesses/service"
"netidhub-saas-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
utilRes "netidhub-saas-be/utils/response"
utilVal "netidhub-saas-be/utils/validator"
)
type userLevelMenuAccessesController struct {
userLevelMenuAccessesService service.UserLevelMenuAccessesService
}
type UserLevelMenuAccessesController interface {
All(c *fiber.Ctx) error
GetByUserLevelId(c *fiber.Ctx) error
GetByMenuId(c *fiber.Ctx) error
CheckAccess(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
SaveBatch(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
}
func NewUserLevelMenuAccessesController(userLevelMenuAccessesService service.UserLevelMenuAccessesService) UserLevelMenuAccessesController {
return &userLevelMenuAccessesController{
userLevelMenuAccessesService: userLevelMenuAccessesService,
}
}
// All UserLevelMenuAccesses
// @Summary Get all UserLevelMenuAccesses
// @Description API for getting all UserLevelMenuAccesses
// @Tags UserLevelMenuAccesses
// @Security Bearer
// @Param user_level_id query int false "User Level ID"
// @Param menu_id query int false "Menu ID"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-accesses [get]
func (_i *userLevelMenuAccessesController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
req := request.UserLevelMenuAccessesQueryRequest{
Pagination: paginate,
}
if userLevelId := c.Query("user_level_id"); userLevelId != "" {
id, _ := strconv.ParseUint(userLevelId, 10, 0)
userLevelIdUint := uint(id)
req.UserLevelId = &userLevelIdUint
}
if menuId := c.Query("menu_id"); menuId != "" {
id, _ := strconv.ParseUint(menuId, 10, 0)
menuIdUint := uint(id)
req.MenuId = &menuIdUint
}
accessesData, paging, err := _i.userLevelMenuAccessesService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuAccesses list successfully retrieved"},
Data: accessesData,
Meta: paging,
})
}
// GetByUserLevelId get UserLevelMenuAccesses by User Level ID
// @Summary Get UserLevelMenuAccesses by User Level ID
// @Description API for getting UserLevelMenuAccesses by User Level ID
// @Tags UserLevelMenuAccesses
// @Security Bearer
// @Param user_level_id path int true "User Level ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-accesses/user-level/{user_level_id} [get]
func (_i *userLevelMenuAccessesController) GetByUserLevelId(c *fiber.Ctx) error {
userLevelId, err := strconv.ParseUint(c.Params("user_level_id"), 10, 0)
if err != nil {
return err
}
accessesData, err := _i.userLevelMenuAccessesService.GetByUserLevelId(uint(userLevelId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuAccesses by user level successfully retrieved"},
Data: accessesData,
})
}
// GetByMenuId get UserLevelMenuAccesses by Menu ID
// @Summary Get UserLevelMenuAccesses by Menu ID
// @Description API for getting UserLevelMenuAccesses by Menu ID
// @Tags UserLevelMenuAccesses
// @Security Bearer
// @Param menu_id path int true "Menu ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-accesses/menu/{menu_id} [get]
func (_i *userLevelMenuAccessesController) GetByMenuId(c *fiber.Ctx) error {
menuId, err := strconv.ParseUint(c.Params("menu_id"), 10, 0)
if err != nil {
return err
}
accessesData, err := _i.userLevelMenuAccessesService.GetByMenuId(uint(menuId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuAccesses by menu successfully retrieved"},
Data: accessesData,
})
}
// CheckAccess check if user level has access to menu
// @Summary Check User Level Menu Access
// @Description API for checking if user level has access to menu
// @Tags UserLevelMenuAccesses
// @Security Bearer
// @Param user_level_id path int true "User Level ID"
// @Param menu_id path int true "Menu ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-accesses/check/{user_level_id}/{menu_id} [get]
func (_i *userLevelMenuAccessesController) CheckAccess(c *fiber.Ctx) error {
userLevelId, err := strconv.ParseUint(c.Params("user_level_id"), 10, 0)
if err != nil {
return err
}
menuId, err := strconv.ParseUint(c.Params("menu_id"), 10, 0)
if err != nil {
return err
}
hasAccess, err := _i.userLevelMenuAccessesService.CheckAccess(uint(userLevelId), uint(menuId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Access check completed"},
Data: fiber.Map{"has_access": hasAccess},
})
}
// Show get one UserLevelMenuAccess
// @Summary Get one UserLevelMenuAccess
// @Description API for getting one UserLevelMenuAccess
// @Tags UserLevelMenuAccesses
// @Security Bearer
// @Param id path int true "UserLevelMenuAccess ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-accesses/{id} [get]
func (_i *userLevelMenuAccessesController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
accessData, err := _i.userLevelMenuAccessesService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuAccess successfully retrieved"},
Data: accessData,
})
}
// Save create UserLevelMenuAccess
// @Summary Create UserLevelMenuAccess
// @Description API for create UserLevelMenuAccess
// @Tags UserLevelMenuAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.UserLevelMenuAccessesCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-accesses [post]
func (_i *userLevelMenuAccessesController) Save(c *fiber.Ctx) error {
req := new(request.UserLevelMenuAccessesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.userLevelMenuAccessesService.Save(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuAccess successfully created"},
})
}
// SaveBatch create UserLevelMenuAccesses batch
// @Summary Create UserLevelMenuAccesses batch
// @Description API for create UserLevelMenuAccesses batch
// @Tags UserLevelMenuAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.UserLevelMenuAccessesBatchCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-accesses/batch [post]
func (_i *userLevelMenuAccessesController) SaveBatch(c *fiber.Ctx) error {
req := new(request.UserLevelMenuAccessesBatchCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.userLevelMenuAccessesService.SaveBatch(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuAccesses batch successfully created"},
})
}
// Update UserLevelMenuAccess
// @Summary Update UserLevelMenuAccess
// @Description API for update UserLevelMenuAccess
// @Tags UserLevelMenuAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param id path int true "UserLevelMenuAccess ID"
// @Param payload body request.UserLevelMenuAccessesUpdateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-accesses/{id} [put]
func (_i *userLevelMenuAccessesController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.UserLevelMenuAccessesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.userLevelMenuAccessesService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuAccess successfully updated"},
})
}
// Delete UserLevelMenuAccess
// @Summary Delete UserLevelMenuAccess
// @Description API for delete UserLevelMenuAccess
// @Tags UserLevelMenuAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param id path int true "UserLevelMenuAccess ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-accesses/{id} [delete]
func (_i *userLevelMenuAccessesController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.userLevelMenuAccessesService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuAccess successfully deleted"},
})
}

View File

@ -0,0 +1,22 @@
package mapper
import (
"netidhub-saas-be/app/database/entity"
res "netidhub-saas-be/app/module/user_level_menu_accesses/response"
)
func UserLevelMenuAccessesResponseMapper(accessReq *entity.UserLevelMenuAccesses) (accessRes *res.UserLevelMenuAccessesResponse) {
if accessReq != nil {
accessRes = &res.UserLevelMenuAccessesResponse{
ID: accessReq.ID,
UserLevelId: accessReq.UserLevelId,
MenuId: accessReq.MenuId,
CanAccess: accessReq.CanAccess,
IsActive: accessReq.IsActive,
CreatedAt: accessReq.CreatedAt,
UpdatedAt: accessReq.UpdatedAt,
}
}
return accessRes
}

View File

@ -0,0 +1,161 @@
package repository
import (
"fmt"
"netidhub-saas-be/app/database"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/user_level_menu_accesses/request"
"netidhub-saas-be/utils/paginator"
"gorm.io/gorm"
)
type userLevelMenuAccessesRepository struct {
DB *database.Database
}
// UserLevelMenuAccessesRepository define interface of IUserLevelMenuAccessesRepository
type UserLevelMenuAccessesRepository interface {
GetAll(req request.UserLevelMenuAccessesQueryRequest) (accesses []*entity.UserLevelMenuAccesses, paging paginator.Pagination, err error)
GetByUserLevelId(userLevelId uint) (accesses []*entity.UserLevelMenuAccesses, err error)
GetByMenuId(menuId uint) (accesses []*entity.UserLevelMenuAccesses, err error)
CheckAccess(userLevelId uint, menuId uint) (hasAccess bool, err error)
FindOne(id uint) (access *entity.UserLevelMenuAccesses, err error)
Create(access *entity.UserLevelMenuAccesses) (err error)
CreateBatch(accesses []*entity.UserLevelMenuAccesses) (err error)
Update(id uint, access *entity.UserLevelMenuAccesses) (err error)
Delete(id uint) (err error)
DeleteByUserLevelId(userLevelId uint) (err error)
DeleteByMenuId(menuId uint) (err error)
}
func NewUserLevelMenuAccessesRepository(db *database.Database) UserLevelMenuAccessesRepository {
return &userLevelMenuAccessesRepository{
DB: db,
}
}
// implement interface of IUserLevelMenuAccessesRepository
func (_i *userLevelMenuAccessesRepository) GetAll(req request.UserLevelMenuAccessesQueryRequest) (accesses []*entity.UserLevelMenuAccesses, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.UserLevelMenuAccesses{})
query = query.Where("is_active = ?", true)
if req.UserLevelId != nil {
query = query.Where("user_level_id = ?", req.UserLevelId)
}
if req.MenuId != nil {
query = query.Where("menu_id = ?", req.MenuId)
}
if req.ClientId != nil {
query = query.Where("client_id = ?", req.ClientId)
}
if req.CanAccess != nil {
query = query.Where("can_access = ?", req.CanAccess)
}
// Preload relations
query = query.Preload("UserLevel").Preload("Menu")
query.Count(&count)
// Apply sorting
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
// Apply pagination (manual calculation like articles)
page := req.Pagination.Page
limit := req.Pagination.Limit
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 10
}
offset := (page - 1) * limit
err = query.Offset(offset).Limit(limit).Find(&accesses).Error
if err != nil {
return
}
// Create pagination response
paging = paginator.Pagination{
Page: page,
Limit: limit,
Count: count,
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
}
return
}
func (_i *userLevelMenuAccessesRepository) GetByUserLevelId(userLevelId uint) (accesses []*entity.UserLevelMenuAccesses, err error) {
query := _i.DB.DB.Model(&entity.UserLevelMenuAccesses{})
query = query.Where("user_level_id = ? AND is_active = ?", userLevelId, true)
query = query.Preload("Menu")
err = query.Find(&accesses).Error
return
}
func (_i *userLevelMenuAccessesRepository) GetByMenuId(menuId uint) (accesses []*entity.UserLevelMenuAccesses, err error) {
query := _i.DB.DB.Model(&entity.UserLevelMenuAccesses{})
query = query.Where("menu_id = ? AND is_active = ?", menuId, true)
query = query.Preload("UserLevel")
err = query.Find(&accesses).Error
return
}
func (_i *userLevelMenuAccessesRepository) CheckAccess(userLevelId uint, menuId uint) (hasAccess bool, err error) {
var access entity.UserLevelMenuAccesses
query := _i.DB.DB.Model(&entity.UserLevelMenuAccesses{})
query = query.Where("user_level_id = ? AND menu_id = ? AND is_active = ? AND can_access = ?", userLevelId, menuId, true, true)
err = query.First(&access).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return false, nil
}
return false, err
}
return true, nil
}
func (_i *userLevelMenuAccessesRepository) FindOne(id uint) (access *entity.UserLevelMenuAccesses, err error) {
query := _i.DB.DB.Preload("UserLevel").Preload("Menu")
if err := query.First(&access, id).Error; err != nil {
return nil, err
}
return access, nil
}
func (_i *userLevelMenuAccessesRepository) Create(access *entity.UserLevelMenuAccesses) (err error) {
return _i.DB.DB.Create(access).Error
}
func (_i *userLevelMenuAccessesRepository) CreateBatch(accesses []*entity.UserLevelMenuAccesses) (err error) {
return _i.DB.DB.Create(&accesses).Error
}
func (_i *userLevelMenuAccessesRepository) Update(id uint, access *entity.UserLevelMenuAccesses) (err error) {
return _i.DB.DB.Model(&entity.UserLevelMenuAccesses{}).
Where(&entity.UserLevelMenuAccesses{ID: id}).
Updates(access).Error
}
func (_i *userLevelMenuAccessesRepository) Delete(id uint) (err error) {
return _i.DB.DB.Delete(&entity.UserLevelMenuAccesses{}, id).Error
}
func (_i *userLevelMenuAccessesRepository) DeleteByUserLevelId(userLevelId uint) (err error) {
return _i.DB.DB.Where("user_level_id = ?", userLevelId).Delete(&entity.UserLevelMenuAccesses{}).Error
}
func (_i *userLevelMenuAccessesRepository) DeleteByMenuId(menuId uint) (err error) {
return _i.DB.DB.Where("menu_id = ?", menuId).Delete(&entity.UserLevelMenuAccesses{}).Error
}

View File

@ -0,0 +1,64 @@
package request
import (
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/utils/paginator"
"github.com/google/uuid"
)
type UserLevelMenuAccessesQueryRequest struct {
UserLevelId *uint `query:"user_level_id"`
MenuId *uint `query:"menu_id"`
ClientId *uuid.UUID `query:"client_id"`
CanAccess *bool `query:"can_access"`
Pagination *paginator.Pagination `query:"pagination"`
}
type UserLevelMenuAccessesCreateRequest struct {
UserLevelId uint `json:"userLevelId" validate:"required"`
MenuId uint `json:"menuId" validate:"required"`
CanAccess bool `json:"canAccess"`
ClientId *uuid.UUID `json:"clientId"`
IsActive *bool `json:"isActive"`
}
func (req UserLevelMenuAccessesCreateRequest) ToEntity() *entity.UserLevelMenuAccesses {
return &entity.UserLevelMenuAccesses{
UserLevelId: req.UserLevelId,
MenuId: req.MenuId,
CanAccess: req.CanAccess,
ClientId: req.ClientId,
IsActive: req.IsActive,
}
}
type UserLevelMenuAccessesUpdateRequest struct {
UserLevelId *uint `json:"userLevelId"`
MenuId *uint `json:"menuId"`
CanAccess *bool `json:"canAccess"`
ClientId *uuid.UUID `json:"clientId"`
IsActive *bool `json:"isActive"`
}
func (req UserLevelMenuAccessesUpdateRequest) ToEntity() *entity.UserLevelMenuAccesses {
access := &entity.UserLevelMenuAccesses{}
if req.UserLevelId != nil {
access.UserLevelId = *req.UserLevelId
}
if req.MenuId != nil {
access.MenuId = *req.MenuId
}
if req.CanAccess != nil {
access.CanAccess = *req.CanAccess
}
access.ClientId = req.ClientId
access.IsActive = req.IsActive
return access
}
type UserLevelMenuAccessesBatchCreateRequest struct {
UserLevelId uint `json:"userLevelId" validate:"required"`
MenuIds []uint `json:"menuIds" validate:"required,min=1"`
ClientId *uuid.UUID `json:"clientId"`
}

View File

@ -0,0 +1,14 @@
package response
import "time"
type UserLevelMenuAccessesResponse struct {
ID uint `json:"id"`
UserLevelId uint `json:"userLevelId"`
MenuId uint `json:"menuId"`
CanAccess bool `json:"canAccess"`
IsActive *bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -0,0 +1,151 @@
package service
import (
"github.com/rs/zerolog"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/user_level_menu_accesses/mapper"
"netidhub-saas-be/app/module/user_level_menu_accesses/repository"
"netidhub-saas-be/app/module/user_level_menu_accesses/request"
"netidhub-saas-be/app/module/user_level_menu_accesses/response"
"netidhub-saas-be/utils/paginator"
)
// UserLevelMenuAccessesService
type userLevelMenuAccessesService struct {
Repo repository.UserLevelMenuAccessesRepository
Log zerolog.Logger
}
// UserLevelMenuAccessesService define interface of IUserLevelMenuAccessesService
type UserLevelMenuAccessesService interface {
All(req request.UserLevelMenuAccessesQueryRequest) (accesses []*response.UserLevelMenuAccessesResponse, paging paginator.Pagination, err error)
GetByUserLevelId(userLevelId uint) (accesses []*response.UserLevelMenuAccessesResponse, err error)
GetByMenuId(menuId uint) (accesses []*response.UserLevelMenuAccessesResponse, err error)
CheckAccess(userLevelId uint, menuId uint) (hasAccess bool, err error)
Show(id uint) (access *response.UserLevelMenuAccessesResponse, err error)
Save(req request.UserLevelMenuAccessesCreateRequest) (err error)
SaveBatch(req request.UserLevelMenuAccessesBatchCreateRequest) (err error)
Update(id uint, req request.UserLevelMenuAccessesUpdateRequest) (err error)
Delete(id uint) error
}
// NewUserLevelMenuAccessesService init UserLevelMenuAccessesService
func NewUserLevelMenuAccessesService(repo repository.UserLevelMenuAccessesRepository, log zerolog.Logger) UserLevelMenuAccessesService {
return &userLevelMenuAccessesService{
Repo: repo,
Log: log,
}
}
// All implement interface of UserLevelMenuAccessesService
func (_i *userLevelMenuAccessesService) All(req request.UserLevelMenuAccessesQueryRequest) (accesses []*response.UserLevelMenuAccessesResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
for _, result := range results {
accesses = append(accesses, mapper.UserLevelMenuAccessesResponseMapper(result))
}
return
}
func (_i *userLevelMenuAccessesService) GetByUserLevelId(userLevelId uint) (accesses []*response.UserLevelMenuAccessesResponse, err error) {
results, err := _i.Repo.GetByUserLevelId(userLevelId)
if err != nil {
return nil, err
}
for _, result := range results {
accesses = append(accesses, mapper.UserLevelMenuAccessesResponseMapper(result))
}
return
}
func (_i *userLevelMenuAccessesService) GetByMenuId(menuId uint) (accesses []*response.UserLevelMenuAccessesResponse, err error) {
results, err := _i.Repo.GetByMenuId(menuId)
if err != nil {
return nil, err
}
for _, result := range results {
accesses = append(accesses, mapper.UserLevelMenuAccessesResponseMapper(result))
}
return
}
func (_i *userLevelMenuAccessesService) CheckAccess(userLevelId uint, menuId uint) (hasAccess bool, err error) {
return _i.Repo.CheckAccess(userLevelId, menuId)
}
func (_i *userLevelMenuAccessesService) Show(id uint) (access *response.UserLevelMenuAccessesResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
return mapper.UserLevelMenuAccessesResponseMapper(result), nil
}
func (_i *userLevelMenuAccessesService) Save(req request.UserLevelMenuAccessesCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Creating user level menu access")
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
access := req.ToEntity()
access.IsActive = &isActive
return _i.Repo.Create(access)
}
func (_i *userLevelMenuAccessesService) SaveBatch(req request.UserLevelMenuAccessesBatchCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Creating user level menu accesses batch")
isActive := true
var accesses []*entity.UserLevelMenuAccesses
for _, menuId := range req.MenuIds {
access := &entity.UserLevelMenuAccesses{
UserLevelId: req.UserLevelId,
MenuId: menuId,
CanAccess: true,
ClientId: req.ClientId,
IsActive: &isActive,
}
accesses = append(accesses, access)
}
return _i.Repo.CreateBatch(accesses)
}
func (_i *userLevelMenuAccessesService) Update(id uint, req request.UserLevelMenuAccessesUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Updating user level menu access")
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
access := req.ToEntity()
access.IsActive = &isActive
return _i.Repo.Update(id, access)
}
func (_i *userLevelMenuAccessesService) Delete(id uint) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
isActive := false
result.IsActive = &isActive
return _i.Repo.Update(id, result)
}

View File

@ -0,0 +1,58 @@
package user_level_menu_accesses
import (
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
"netidhub-saas-be/app/module/user_level_menu_accesses/controller"
"netidhub-saas-be/app/module/user_level_menu_accesses/repository"
"netidhub-saas-be/app/module/user_level_menu_accesses/service"
)
// struct of UserLevelMenuAccessesRouter
type UserLevelMenuAccessesRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of UserLevelMenuAccesses module
var NewUserLevelMenuAccessesModule = fx.Options(
// register repository of UserLevelMenuAccesses module
fx.Provide(repository.NewUserLevelMenuAccessesRepository),
// register service of UserLevelMenuAccesses module
fx.Provide(service.NewUserLevelMenuAccessesService),
// register controller of UserLevelMenuAccesses module
fx.Provide(controller.NewController),
// register router of UserLevelMenuAccesses module
fx.Provide(NewUserLevelMenuAccessesRouter),
)
// init UserLevelMenuAccessesRouter
func NewUserLevelMenuAccessesRouter(fiber *fiber.App, controller *controller.Controller) *UserLevelMenuAccessesRouter {
return &UserLevelMenuAccessesRouter{
App: fiber,
Controller: controller,
}
}
// register routes of UserLevelMenuAccesses module
func (_i *UserLevelMenuAccessesRouter) RegisterUserLevelMenuAccessesRoutes() {
// define controllers
userLevelMenuAccessesController := _i.Controller.UserLevelMenuAccesses
// define routes
_i.App.Route("/user-level-menu-accesses", func(router fiber.Router) {
router.Get("/", userLevelMenuAccessesController.All)
router.Get("/:id", userLevelMenuAccessesController.Show)
router.Get("/user-level/:user_level_id", userLevelMenuAccessesController.GetByUserLevelId)
router.Get("/menu/:menu_id", userLevelMenuAccessesController.GetByMenuId)
router.Get("/check/:user_level_id/:menu_id", userLevelMenuAccessesController.CheckAccess)
router.Post("/", userLevelMenuAccessesController.Save)
router.Post("/batch", userLevelMenuAccessesController.SaveBatch)
router.Put("/:id", userLevelMenuAccessesController.Update)
router.Delete("/:id", userLevelMenuAccessesController.Delete)
})
}

View File

@ -0,0 +1,16 @@
package controller
import (
"netidhub-saas-be/app/module/user_level_menu_action_accesses/service"
)
type Controller struct {
UserLevelMenuActionAccesses UserLevelMenuActionAccessesController
}
func NewController(userLevelMenuActionAccessesService service.UserLevelMenuActionAccessesService) *Controller {
return &Controller{
UserLevelMenuActionAccesses: NewUserLevelMenuActionAccessesController(userLevelMenuActionAccessesService),
}
}

View File

@ -0,0 +1,379 @@
package controller
import (
"netidhub-saas-be/app/module/user_level_menu_action_accesses/request"
"netidhub-saas-be/app/module/user_level_menu_action_accesses/service"
"netidhub-saas-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
utilRes "netidhub-saas-be/utils/response"
utilVal "netidhub-saas-be/utils/validator"
)
type userLevelMenuActionAccessesController struct {
userLevelMenuActionAccessesService service.UserLevelMenuActionAccessesService
}
type UserLevelMenuActionAccessesController interface {
All(c *fiber.Ctx) error
GetByUserLevelId(c *fiber.Ctx) error
GetByUserLevelIdAndMenuId(c *fiber.Ctx) error
GetByMenuId(c *fiber.Ctx) error
CheckAccess(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
SaveBatch(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
}
func NewUserLevelMenuActionAccessesController(userLevelMenuActionAccessesService service.UserLevelMenuActionAccessesService) UserLevelMenuActionAccessesController {
return &userLevelMenuActionAccessesController{
userLevelMenuActionAccessesService: userLevelMenuActionAccessesService,
}
}
// All UserLevelMenuActionAccesses
// @Summary Get all UserLevelMenuActionAccesses
// @Description API for getting all UserLevelMenuActionAccesses
// @Tags UserLevelMenuActionAccesses
// @Security Bearer
// @Param user_level_id query int false "User Level ID"
// @Param menu_id query int false "Menu ID"
// @Param action_code query string false "Action Code"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-action-accesses [get]
func (_i *userLevelMenuActionAccessesController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
req := request.UserLevelMenuActionAccessesQueryRequest{
Pagination: paginate,
}
if userLevelId := c.Query("user_level_id"); userLevelId != "" {
id, _ := strconv.ParseUint(userLevelId, 10, 0)
userLevelIdUint := uint(id)
req.UserLevelId = &userLevelIdUint
}
if menuId := c.Query("menu_id"); menuId != "" {
id, _ := strconv.ParseUint(menuId, 10, 0)
menuIdUint := uint(id)
req.MenuId = &menuIdUint
}
if actionCode := c.Query("action_code"); actionCode != "" {
req.ActionCode = &actionCode
}
accessesData, paging, err := _i.userLevelMenuActionAccessesService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuActionAccesses list successfully retrieved"},
Data: accessesData,
Meta: paging,
})
}
// GetByUserLevelId get UserLevelMenuActionAccesses by User Level ID
// @Summary Get UserLevelMenuActionAccesses by User Level ID
// @Description API for getting UserLevelMenuActionAccesses by User Level ID
// @Tags UserLevelMenuActionAccesses
// @Security Bearer
// @Param user_level_id path int true "User Level ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-action-accesses/user-level/{user_level_id} [get]
func (_i *userLevelMenuActionAccessesController) GetByUserLevelId(c *fiber.Ctx) error {
userLevelId, err := strconv.ParseUint(c.Params("user_level_id"), 10, 0)
if err != nil {
return err
}
accessesData, err := _i.userLevelMenuActionAccessesService.GetByUserLevelId(uint(userLevelId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuActionAccesses by user level successfully retrieved"},
Data: accessesData,
})
}
// GetByUserLevelIdAndMenuId get UserLevelMenuActionAccesses by User Level ID and Menu ID
// @Summary Get UserLevelMenuActionAccesses by User Level ID and Menu ID
// @Description API for getting UserLevelMenuActionAccesses by User Level ID and Menu ID
// @Tags UserLevelMenuActionAccesses
// @Security Bearer
// @Param user_level_id path int true "User Level ID"
// @Param menu_id path int true "Menu ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-action-accesses/user-level/{user_level_id}/menu/{menu_id} [get]
func (_i *userLevelMenuActionAccessesController) GetByUserLevelIdAndMenuId(c *fiber.Ctx) error {
userLevelId, err := strconv.ParseUint(c.Params("user_level_id"), 10, 0)
if err != nil {
return err
}
menuId, err := strconv.ParseUint(c.Params("menu_id"), 10, 0)
if err != nil {
return err
}
accessesData, err := _i.userLevelMenuActionAccessesService.GetByUserLevelIdAndMenuId(uint(userLevelId), uint(menuId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuActionAccesses by user level and menu successfully retrieved"},
Data: accessesData,
})
}
// GetByMenuId get UserLevelMenuActionAccesses by Menu ID
// @Summary Get UserLevelMenuActionAccesses by Menu ID
// @Description API for getting UserLevelMenuActionAccesses by Menu ID
// @Tags UserLevelMenuActionAccesses
// @Security Bearer
// @Param menu_id path int true "Menu ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-action-accesses/menu/{menu_id} [get]
func (_i *userLevelMenuActionAccessesController) GetByMenuId(c *fiber.Ctx) error {
menuId, err := strconv.ParseUint(c.Params("menu_id"), 10, 0)
if err != nil {
return err
}
accessesData, err := _i.userLevelMenuActionAccessesService.GetByMenuId(uint(menuId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuActionAccesses by menu successfully retrieved"},
Data: accessesData,
})
}
// CheckAccess check if user level has access to action in menu
// @Summary Check User Level Menu Action Access
// @Description API for checking if user level has access to action in menu
// @Tags UserLevelMenuActionAccesses
// @Security Bearer
// @Param user_level_id path int true "User Level ID"
// @Param menu_id path int true "Menu ID"
// @Param action_code path string true "Action Code"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-action-accesses/check/{user_level_id}/{menu_id}/{action_code} [get]
func (_i *userLevelMenuActionAccessesController) CheckAccess(c *fiber.Ctx) error {
userLevelId, err := strconv.ParseUint(c.Params("user_level_id"), 10, 0)
if err != nil {
return err
}
menuId, err := strconv.ParseUint(c.Params("menu_id"), 10, 0)
if err != nil {
return err
}
actionCode := c.Params("action_code")
if actionCode == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"code": 400,
"messages": []string{"Action code is required"},
})
}
hasAccess, err := _i.userLevelMenuActionAccessesService.CheckAccess(uint(userLevelId), uint(menuId), actionCode)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Access check completed"},
Data: fiber.Map{"has_access": hasAccess},
})
}
// Show get one UserLevelMenuActionAccess
// @Summary Get one UserLevelMenuActionAccess
// @Description API for getting one UserLevelMenuActionAccess
// @Tags UserLevelMenuActionAccesses
// @Security Bearer
// @Param id path int true "UserLevelMenuActionAccess ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-action-accesses/{id} [get]
func (_i *userLevelMenuActionAccessesController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
accessData, err := _i.userLevelMenuActionAccessesService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuActionAccess successfully retrieved"},
Data: accessData,
})
}
// Save create UserLevelMenuActionAccess
// @Summary Create UserLevelMenuActionAccess
// @Description API for create UserLevelMenuActionAccess
// @Tags UserLevelMenuActionAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.UserLevelMenuActionAccessesCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-action-accesses [post]
func (_i *userLevelMenuActionAccessesController) Save(c *fiber.Ctx) error {
req := new(request.UserLevelMenuActionAccessesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.userLevelMenuActionAccessesService.Save(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuActionAccess successfully created"},
})
}
// SaveBatch create UserLevelMenuActionAccesses batch
// @Summary Create UserLevelMenuActionAccesses batch
// @Description API for create UserLevelMenuActionAccesses batch
// @Tags UserLevelMenuActionAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.UserLevelMenuActionAccessesBatchCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-action-accesses/batch [post]
func (_i *userLevelMenuActionAccessesController) SaveBatch(c *fiber.Ctx) error {
req := new(request.UserLevelMenuActionAccessesBatchCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.userLevelMenuActionAccessesService.SaveBatch(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuActionAccesses batch successfully created"},
})
}
// Update UserLevelMenuActionAccess
// @Summary Update UserLevelMenuActionAccess
// @Description API for update UserLevelMenuActionAccess
// @Tags UserLevelMenuActionAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param id path int true "UserLevelMenuActionAccess ID"
// @Param payload body request.UserLevelMenuActionAccessesUpdateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-action-accesses/{id} [put]
func (_i *userLevelMenuActionAccessesController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.UserLevelMenuActionAccessesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.userLevelMenuActionAccessesService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuActionAccess successfully updated"},
})
}
// Delete UserLevelMenuActionAccess
// @Summary Delete UserLevelMenuActionAccess
// @Description API for delete UserLevelMenuActionAccess
// @Tags UserLevelMenuActionAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param id path int true "UserLevelMenuActionAccess ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-menu-action-accesses/{id} [delete]
func (_i *userLevelMenuActionAccessesController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.userLevelMenuActionAccessesService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelMenuActionAccess successfully deleted"},
})
}

View File

@ -0,0 +1,23 @@
package mapper
import (
"netidhub-saas-be/app/database/entity"
res "netidhub-saas-be/app/module/user_level_menu_action_accesses/response"
)
func UserLevelMenuActionAccessesResponseMapper(accessReq *entity.UserLevelMenuActionAccesses) (accessRes *res.UserLevelMenuActionAccessesResponse) {
if accessReq != nil {
accessRes = &res.UserLevelMenuActionAccessesResponse{
ID: accessReq.ID,
UserLevelId: accessReq.UserLevelId,
MenuId: accessReq.MenuId,
ActionCode: accessReq.ActionCode,
CanAccess: accessReq.CanAccess,
IsActive: accessReq.IsActive,
CreatedAt: accessReq.CreatedAt,
UpdatedAt: accessReq.UpdatedAt,
}
}
return accessRes
}

View File

@ -0,0 +1,178 @@
package repository
import (
"fmt"
"netidhub-saas-be/app/database"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/user_level_menu_action_accesses/request"
"netidhub-saas-be/utils/paginator"
"gorm.io/gorm"
)
type userLevelMenuActionAccessesRepository struct {
DB *database.Database
}
// UserLevelMenuActionAccessesRepository define interface of IUserLevelMenuActionAccessesRepository
type UserLevelMenuActionAccessesRepository interface {
GetAll(req request.UserLevelMenuActionAccessesQueryRequest) (accesses []*entity.UserLevelMenuActionAccesses, paging paginator.Pagination, err error)
GetByUserLevelId(userLevelId uint) (accesses []*entity.UserLevelMenuActionAccesses, err error)
GetByUserLevelIdAndMenuId(userLevelId uint, menuId uint) (accesses []*entity.UserLevelMenuActionAccesses, err error)
GetByMenuId(menuId uint) (accesses []*entity.UserLevelMenuActionAccesses, err error)
CheckAccess(userLevelId uint, menuId uint, actionCode string) (hasAccess bool, err error)
FindOne(id uint) (access *entity.UserLevelMenuActionAccesses, err error)
Create(access *entity.UserLevelMenuActionAccesses) (err error)
CreateBatch(accesses []*entity.UserLevelMenuActionAccesses) (err error)
Update(id uint, access *entity.UserLevelMenuActionAccesses) (err error)
Delete(id uint) (err error)
DeleteByUserLevelId(userLevelId uint) (err error)
DeleteByMenuId(menuId uint) (err error)
DeleteByUserLevelIdAndMenuId(userLevelId uint, menuId uint) (err error)
}
func NewUserLevelMenuActionAccessesRepository(db *database.Database) UserLevelMenuActionAccessesRepository {
return &userLevelMenuActionAccessesRepository{
DB: db,
}
}
// implement interface of IUserLevelMenuActionAccessesRepository
func (_i *userLevelMenuActionAccessesRepository) GetAll(req request.UserLevelMenuActionAccessesQueryRequest) (accesses []*entity.UserLevelMenuActionAccesses, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.UserLevelMenuActionAccesses{})
query = query.Where("is_active = ?", true)
if req.UserLevelId != nil {
query = query.Where("user_level_id = ?", req.UserLevelId)
}
if req.MenuId != nil {
query = query.Where("menu_id = ?", req.MenuId)
}
if req.ActionCode != nil && *req.ActionCode != "" {
query = query.Where("action_code = ?", req.ActionCode)
}
if req.ClientId != nil {
query = query.Where("client_id = ?", req.ClientId)
}
if req.CanAccess != nil {
query = query.Where("can_access = ?", req.CanAccess)
}
// Preload relations
query = query.Preload("UserLevel").Preload("Menu")
query.Count(&count)
// Apply sorting
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
// Apply pagination (manual calculation like articles)
page := req.Pagination.Page
limit := req.Pagination.Limit
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 10
}
offset := (page - 1) * limit
err = query.Offset(offset).Limit(limit).Find(&accesses).Error
if err != nil {
return
}
// Create pagination response
paging = paginator.Pagination{
Page: page,
Limit: limit,
Count: count,
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
}
return
}
func (_i *userLevelMenuActionAccessesRepository) GetByUserLevelId(userLevelId uint) (accesses []*entity.UserLevelMenuActionAccesses, err error) {
query := _i.DB.DB.Model(&entity.UserLevelMenuActionAccesses{})
query = query.Where("user_level_id = ? AND is_active = ?", userLevelId, true)
query = query.Preload("Menu")
err = query.Find(&accesses).Error
return
}
func (_i *userLevelMenuActionAccessesRepository) GetByUserLevelIdAndMenuId(userLevelId uint, menuId uint) (accesses []*entity.UserLevelMenuActionAccesses, err error) {
query := _i.DB.DB.Model(&entity.UserLevelMenuActionAccesses{})
query = query.Where("user_level_id = ? AND menu_id = ? AND is_active = ?", userLevelId, menuId, true)
query = query.Preload("Menu")
err = query.Find(&accesses).Error
return
}
func (_i *userLevelMenuActionAccessesRepository) GetByMenuId(menuId uint) (accesses []*entity.UserLevelMenuActionAccesses, err error) {
query := _i.DB.DB.Model(&entity.UserLevelMenuActionAccesses{})
query = query.Where("menu_id = ? AND is_active = ?", menuId, true)
query = query.Preload("UserLevel")
err = query.Find(&accesses).Error
return
}
func (_i *userLevelMenuActionAccessesRepository) CheckAccess(userLevelId uint, menuId uint, actionCode string) (hasAccess bool, err error) {
var access entity.UserLevelMenuActionAccesses
query := _i.DB.DB.Model(&entity.UserLevelMenuActionAccesses{})
query = query.Where("user_level_id = ? AND menu_id = ? AND action_code = ? AND is_active = ? AND can_access = ?", userLevelId, menuId, actionCode, true, true)
err = query.First(&access).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return false, nil
}
return false, err
}
return true, nil
}
func (_i *userLevelMenuActionAccessesRepository) FindOne(id uint) (access *entity.UserLevelMenuActionAccesses, err error) {
query := _i.DB.DB.Preload("UserLevel").Preload("Menu")
if err := query.First(&access, id).Error; err != nil {
return nil, err
}
return access, nil
}
func (_i *userLevelMenuActionAccessesRepository) Create(access *entity.UserLevelMenuActionAccesses) (err error) {
return _i.DB.DB.Create(access).Error
}
func (_i *userLevelMenuActionAccessesRepository) CreateBatch(accesses []*entity.UserLevelMenuActionAccesses) (err error) {
return _i.DB.DB.Create(&accesses).Error
}
func (_i *userLevelMenuActionAccessesRepository) Update(id uint, access *entity.UserLevelMenuActionAccesses) (err error) {
return _i.DB.DB.Model(&entity.UserLevelMenuActionAccesses{}).
Where(&entity.UserLevelMenuActionAccesses{ID: id}).
Updates(access).Error
}
func (_i *userLevelMenuActionAccessesRepository) Delete(id uint) (err error) {
return _i.DB.DB.Delete(&entity.UserLevelMenuActionAccesses{}, id).Error
}
func (_i *userLevelMenuActionAccessesRepository) DeleteByUserLevelId(userLevelId uint) (err error) {
return _i.DB.DB.Where("user_level_id = ?", userLevelId).Delete(&entity.UserLevelMenuActionAccesses{}).Error
}
func (_i *userLevelMenuActionAccessesRepository) DeleteByMenuId(menuId uint) (err error) {
return _i.DB.DB.Where("menu_id = ?", menuId).Delete(&entity.UserLevelMenuActionAccesses{}).Error
}
func (_i *userLevelMenuActionAccessesRepository) DeleteByUserLevelIdAndMenuId(userLevelId uint, menuId uint) (err error) {
return _i.DB.DB.Where("user_level_id = ? AND menu_id = ?", userLevelId, menuId).Delete(&entity.UserLevelMenuActionAccesses{}).Error
}

View File

@ -0,0 +1,72 @@
package request
import (
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/utils/paginator"
"github.com/google/uuid"
)
type UserLevelMenuActionAccessesQueryRequest struct {
UserLevelId *uint `query:"user_level_id"`
MenuId *uint `query:"menu_id"`
ActionCode *string `query:"action_code"`
ClientId *uuid.UUID `query:"client_id"`
CanAccess *bool `query:"can_access"`
Pagination *paginator.Pagination `query:"pagination"`
}
type UserLevelMenuActionAccessesCreateRequest struct {
UserLevelId uint `json:"userLevelId" validate:"required"`
MenuId uint `json:"menuId" validate:"required"`
ActionCode string `json:"actionCode" validate:"required"`
CanAccess bool `json:"canAccess"`
ClientId *uuid.UUID `json:"clientId"`
IsActive *bool `json:"isActive"`
}
func (req UserLevelMenuActionAccessesCreateRequest) ToEntity() *entity.UserLevelMenuActionAccesses {
return &entity.UserLevelMenuActionAccesses{
UserLevelId: req.UserLevelId,
MenuId: req.MenuId,
ActionCode: req.ActionCode,
CanAccess: req.CanAccess,
ClientId: req.ClientId,
IsActive: req.IsActive,
}
}
type UserLevelMenuActionAccessesUpdateRequest struct {
UserLevelId *uint `json:"userLevelId"`
MenuId *uint `json:"menuId"`
ActionCode *string `json:"actionCode"`
CanAccess *bool `json:"canAccess"`
ClientId *uuid.UUID `json:"clientId"`
IsActive *bool `json:"isActive"`
}
func (req UserLevelMenuActionAccessesUpdateRequest) ToEntity() *entity.UserLevelMenuActionAccesses {
access := &entity.UserLevelMenuActionAccesses{}
if req.UserLevelId != nil {
access.UserLevelId = *req.UserLevelId
}
if req.MenuId != nil {
access.MenuId = *req.MenuId
}
if req.ActionCode != nil {
access.ActionCode = *req.ActionCode
}
if req.CanAccess != nil {
access.CanAccess = *req.CanAccess
}
access.ClientId = req.ClientId
access.IsActive = req.IsActive
return access
}
type UserLevelMenuActionAccessesBatchCreateRequest struct {
UserLevelId uint `json:"userLevelId" validate:"required"`
MenuId uint `json:"menuId" validate:"required"`
ActionCodes []string `json:"actionCodes" validate:"required,min=1"`
ClientId *uuid.UUID `json:"clientId"`
}

View File

@ -0,0 +1,15 @@
package response
import "time"
type UserLevelMenuActionAccessesResponse struct {
ID uint `json:"id"`
UserLevelId uint `json:"userLevelId"`
MenuId uint `json:"menuId"`
ActionCode string `json:"actionCode"`
CanAccess bool `json:"canAccess"`
IsActive *bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -0,0 +1,166 @@
package service
import (
"github.com/rs/zerolog"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/user_level_menu_action_accesses/mapper"
"netidhub-saas-be/app/module/user_level_menu_action_accesses/repository"
"netidhub-saas-be/app/module/user_level_menu_action_accesses/request"
"netidhub-saas-be/app/module/user_level_menu_action_accesses/response"
"netidhub-saas-be/utils/paginator"
)
// UserLevelMenuActionAccessesService
type userLevelMenuActionAccessesService struct {
Repo repository.UserLevelMenuActionAccessesRepository
Log zerolog.Logger
}
// UserLevelMenuActionAccessesService define interface of IUserLevelMenuActionAccessesService
type UserLevelMenuActionAccessesService interface {
All(req request.UserLevelMenuActionAccessesQueryRequest) (accesses []*response.UserLevelMenuActionAccessesResponse, paging paginator.Pagination, err error)
GetByUserLevelId(userLevelId uint) (accesses []*response.UserLevelMenuActionAccessesResponse, err error)
GetByUserLevelIdAndMenuId(userLevelId uint, menuId uint) (accesses []*response.UserLevelMenuActionAccessesResponse, err error)
GetByMenuId(menuId uint) (accesses []*response.UserLevelMenuActionAccessesResponse, err error)
CheckAccess(userLevelId uint, menuId uint, actionCode string) (hasAccess bool, err error)
Show(id uint) (access *response.UserLevelMenuActionAccessesResponse, err error)
Save(req request.UserLevelMenuActionAccessesCreateRequest) (err error)
SaveBatch(req request.UserLevelMenuActionAccessesBatchCreateRequest) (err error)
Update(id uint, req request.UserLevelMenuActionAccessesUpdateRequest) (err error)
Delete(id uint) error
}
// NewUserLevelMenuActionAccessesService init UserLevelMenuActionAccessesService
func NewUserLevelMenuActionAccessesService(repo repository.UserLevelMenuActionAccessesRepository, log zerolog.Logger) UserLevelMenuActionAccessesService {
return &userLevelMenuActionAccessesService{
Repo: repo,
Log: log,
}
}
// All implement interface of UserLevelMenuActionAccessesService
func (_i *userLevelMenuActionAccessesService) All(req request.UserLevelMenuActionAccessesQueryRequest) (accesses []*response.UserLevelMenuActionAccessesResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
for _, result := range results {
accesses = append(accesses, mapper.UserLevelMenuActionAccessesResponseMapper(result))
}
return
}
func (_i *userLevelMenuActionAccessesService) GetByUserLevelId(userLevelId uint) (accesses []*response.UserLevelMenuActionAccessesResponse, err error) {
results, err := _i.Repo.GetByUserLevelId(userLevelId)
if err != nil {
return nil, err
}
for _, result := range results {
accesses = append(accesses, mapper.UserLevelMenuActionAccessesResponseMapper(result))
}
return
}
func (_i *userLevelMenuActionAccessesService) GetByUserLevelIdAndMenuId(userLevelId uint, menuId uint) (accesses []*response.UserLevelMenuActionAccessesResponse, err error) {
results, err := _i.Repo.GetByUserLevelIdAndMenuId(userLevelId, menuId)
if err != nil {
return nil, err
}
for _, result := range results {
accesses = append(accesses, mapper.UserLevelMenuActionAccessesResponseMapper(result))
}
return
}
func (_i *userLevelMenuActionAccessesService) GetByMenuId(menuId uint) (accesses []*response.UserLevelMenuActionAccessesResponse, err error) {
results, err := _i.Repo.GetByMenuId(menuId)
if err != nil {
return nil, err
}
for _, result := range results {
accesses = append(accesses, mapper.UserLevelMenuActionAccessesResponseMapper(result))
}
return
}
func (_i *userLevelMenuActionAccessesService) CheckAccess(userLevelId uint, menuId uint, actionCode string) (hasAccess bool, err error) {
return _i.Repo.CheckAccess(userLevelId, menuId, actionCode)
}
func (_i *userLevelMenuActionAccessesService) Show(id uint) (access *response.UserLevelMenuActionAccessesResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
return mapper.UserLevelMenuActionAccessesResponseMapper(result), nil
}
func (_i *userLevelMenuActionAccessesService) Save(req request.UserLevelMenuActionAccessesCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Creating user level menu action access")
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
access := req.ToEntity()
access.IsActive = &isActive
return _i.Repo.Create(access)
}
func (_i *userLevelMenuActionAccessesService) SaveBatch(req request.UserLevelMenuActionAccessesBatchCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Creating user level menu action accesses batch")
isActive := true
var accesses []*entity.UserLevelMenuActionAccesses
for _, actionCode := range req.ActionCodes {
access := &entity.UserLevelMenuActionAccesses{
UserLevelId: req.UserLevelId,
MenuId: req.MenuId,
ActionCode: actionCode,
CanAccess: true,
ClientId: req.ClientId,
IsActive: &isActive,
}
accesses = append(accesses, access)
}
return _i.Repo.CreateBatch(accesses)
}
func (_i *userLevelMenuActionAccessesService) Update(id uint, req request.UserLevelMenuActionAccessesUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Updating user level menu action access")
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
access := req.ToEntity()
access.IsActive = &isActive
return _i.Repo.Update(id, access)
}
func (_i *userLevelMenuActionAccessesService) Delete(id uint) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
isActive := false
result.IsActive = &isActive
return _i.Repo.Update(id, result)
}

View File

@ -0,0 +1,59 @@
package user_level_menu_action_accesses
import (
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
"netidhub-saas-be/app/module/user_level_menu_action_accesses/controller"
"netidhub-saas-be/app/module/user_level_menu_action_accesses/repository"
"netidhub-saas-be/app/module/user_level_menu_action_accesses/service"
)
// struct of UserLevelMenuActionAccessesRouter
type UserLevelMenuActionAccessesRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of UserLevelMenuActionAccesses module
var NewUserLevelMenuActionAccessesModule = fx.Options(
// register repository of UserLevelMenuActionAccesses module
fx.Provide(repository.NewUserLevelMenuActionAccessesRepository),
// register service of UserLevelMenuActionAccesses module
fx.Provide(service.NewUserLevelMenuActionAccessesService),
// register controller of UserLevelMenuActionAccesses module
fx.Provide(controller.NewController),
// register router of UserLevelMenuActionAccesses module
fx.Provide(NewUserLevelMenuActionAccessesRouter),
)
// init UserLevelMenuActionAccessesRouter
func NewUserLevelMenuActionAccessesRouter(fiber *fiber.App, controller *controller.Controller) *UserLevelMenuActionAccessesRouter {
return &UserLevelMenuActionAccessesRouter{
App: fiber,
Controller: controller,
}
}
// register routes of UserLevelMenuActionAccesses module
func (_i *UserLevelMenuActionAccessesRouter) RegisterUserLevelMenuActionAccessesRoutes() {
// define controllers
userLevelMenuActionAccessesController := _i.Controller.UserLevelMenuActionAccesses
// define routes
_i.App.Route("/user-level-menu-action-accesses", func(router fiber.Router) {
router.Get("/", userLevelMenuActionAccessesController.All)
router.Get("/:id", userLevelMenuActionAccessesController.Show)
router.Get("/user-level/:user_level_id", userLevelMenuActionAccessesController.GetByUserLevelId)
router.Get("/user-level/:user_level_id/menu/:menu_id", userLevelMenuActionAccessesController.GetByUserLevelIdAndMenuId)
router.Get("/menu/:menu_id", userLevelMenuActionAccessesController.GetByMenuId)
router.Get("/check/:user_level_id/:menu_id/:action_code", userLevelMenuActionAccessesController.CheckAccess)
router.Post("/", userLevelMenuActionAccessesController.Save)
router.Post("/batch", userLevelMenuActionAccessesController.SaveBatch)
router.Put("/:id", userLevelMenuActionAccessesController.Update)
router.Delete("/:id", userLevelMenuActionAccessesController.Delete)
})
}

View File

@ -58,6 +58,7 @@ func (_i *userLevelModuleAccessesRepository) GetAll(req request.UserLevelModuleA
query.Count(&count)
// Apply sorting
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
@ -66,15 +67,29 @@ func (_i *userLevelModuleAccessesRepository) GetAll(req request.UserLevelModuleA
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
// Apply pagination (manual calculation like articles)
page := req.Pagination.Page
limit := req.Pagination.Limit
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 10
}
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&accesses).Error
offset := (page - 1) * limit
err = query.Offset(offset).Limit(limit).Find(&accesses).Error
if err != nil {
return
}
paging = *req.Pagination
// Create pagination response
paging = paginator.Pagination{
Page: page,
Limit: limit,
Count: count,
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
}
return
}

View File

@ -6,11 +6,11 @@ import (
)
type UserLevelModuleAccessesQueryRequest struct {
UserLevelId *uint `query:"user_level_id"`
ModuleId *uint `query:"module_id"`
CanAccess *bool `query:"can_access"`
ClientId *uuid.UUID `query:"client_id"`
Pagination *paginator.Query `query:"pagination"`
UserLevelId *uint `query:"user_level_id"`
ModuleId *uint `query:"module_id"`
CanAccess *bool `query:"can_access"`
ClientId *uuid.UUID `query:"client_id"`
Pagination *paginator.Pagination `query:"pagination"`
}
type UserLevelModuleAccessesCreateRequest struct {

View File

@ -25,6 +25,9 @@ import (
"netidhub-saas-be/app/module/magazines"
"netidhub-saas-be/app/module/master_menus"
"netidhub-saas-be/app/module/master_modules"
"netidhub-saas-be/app/module/menu_actions"
"netidhub-saas-be/app/module/user_level_menu_accesses"
"netidhub-saas-be/app/module/user_level_menu_action_accesses"
"netidhub-saas-be/app/module/provinces"
"netidhub-saas-be/app/module/schedules"
"netidhub-saas-be/app/module/subscription"
@ -65,9 +68,12 @@ type Router struct {
FeedbacksRouter *feedbacks.FeedbacksRouter
MagazineFilesRouter *magazine_files.MagazineFilesRouter
MagazinesRouter *magazines.MagazinesRouter
MasterMenusRouter *master_menus.MasterMenusRouter
MasterModulesRouter *master_modules.MasterModulesRouter
ProvincesRouter *provinces.ProvincesRouter
MasterMenusRouter *master_menus.MasterMenusRouter
MasterModulesRouter *master_modules.MasterModulesRouter
MenuActionsRouter *menu_actions.MenuActionsRouter
UserLevelMenuAccessesRouter *user_level_menu_accesses.UserLevelMenuAccessesRouter
UserLevelMenuActionAccessesRouter *user_level_menu_action_accesses.UserLevelMenuActionAccessesRouter
ProvincesRouter *provinces.ProvincesRouter
SchedulesRouter *schedules.SchedulesRouter
SubscriptionRouter *subscription.SubscriptionRouter
UserLevelsRouter *user_levels.UserLevelsRouter
@ -104,6 +110,9 @@ func NewRouter(
magazinesRouter *magazines.MagazinesRouter,
masterMenuRouter *master_menus.MasterMenusRouter,
masterModuleRouter *master_modules.MasterModulesRouter,
menuActionsRouter *menu_actions.MenuActionsRouter,
userLevelMenuAccessesRouter *user_level_menu_accesses.UserLevelMenuAccessesRouter,
userLevelMenuActionAccessesRouter *user_level_menu_action_accesses.UserLevelMenuActionAccessesRouter,
provincesRouter *provinces.ProvincesRouter,
schedulesRouter *schedules.SchedulesRouter,
subscriptionRouter *subscription.SubscriptionRouter,
@ -137,9 +146,12 @@ func NewRouter(
FeedbacksRouter: feedbacksRouter,
MagazineFilesRouter: magazineFilesRouter,
MagazinesRouter: magazinesRouter,
MasterMenusRouter: masterMenuRouter,
MasterModulesRouter: masterModuleRouter,
ProvincesRouter: provincesRouter,
MasterMenusRouter: masterMenuRouter,
MasterModulesRouter: masterModuleRouter,
MenuActionsRouter: menuActionsRouter,
UserLevelMenuAccessesRouter: userLevelMenuAccessesRouter,
UserLevelMenuActionAccessesRouter: userLevelMenuActionAccessesRouter,
ProvincesRouter: provincesRouter,
SchedulesRouter: schedulesRouter,
SubscriptionRouter: subscriptionRouter,
UserLevelsRouter: userLevelsRouter,
@ -184,6 +196,9 @@ func (r *Router) Register() {
r.MagazineFilesRouter.RegisterMagazineFilesRoutes()
r.MasterMenusRouter.RegisterMasterMenusRoutes()
r.MasterModulesRouter.RegisterMasterModulesRoutes()
r.MenuActionsRouter.RegisterMenuActionsRoutes()
r.UserLevelMenuAccessesRouter.RegisterUserLevelMenuAccessesRoutes()
r.UserLevelMenuActionAccessesRouter.RegisterUserLevelMenuActionAccessesRoutes()
r.ProvincesRouter.RegisterProvincesRoutes()
r.SchedulesRouter.RegisterSchedulesRoutes()
r.SubscriptionRouter.RegisterSubscriptionRoutes()

View File

@ -0,0 +1,311 @@
# 📋 Plan: Menu & Action Access Control System
## 🎯 Tujuan
Membangun sistem akses kontrol yang memungkinkan:
1. **1 Menu memiliki banyak Actions** - Satu menu dapat memiliki berbagai action (view table, create, edit, delete, approve, export, dll)
2. **User Level dapat di-assign Menu** - Setiap user level dapat di-assign menu apa saja yang bisa diakses
3. **User Level dapat di-assign Actions per Menu** - Setiap user level dapat di-assign action apa saja yang bisa dilakukan di setiap menu (contoh: Creator hanya bisa create dan edit, Approver bisa delete)
## 🔍 Analisis Kebutuhan
### Use Case 1: Menu "Content Management"
- **Actions yang tersedia:**
- `view` - Melihat daftar content
- `create` - Membuat content baru
- `edit` - Mengedit content yang ada
- `delete` - Menghapus content
- `approve` - Approve content
- `export` - Export data content
### Use Case 2: User Level "Creator"
- **Menu yang bisa diakses:** Content Management
- **Actions yang bisa dilakukan:**
- ✅ `view` - Bisa melihat daftar
- ✅ `create` - Bisa membuat baru
- ✅ `edit` - Bisa mengedit
- ❌ `delete` - Tidak bisa menghapus
- ❌ `approve` - Tidak bisa approve
- ❌ `export` - Tidak bisa export
### Use Case 3: User Level "Approver"
- **Menu yang bisa diakses:** Content Management
- **Actions yang bisa dilakukan:**
- ✅ `view` - Bisa melihat daftar
- ❌ `create` - Tidak bisa membuat baru
- ❌ `edit` - Tidak bisa mengedit
- ✅ `delete` - Bisa menghapus
- ✅ `approve` - Bisa approve
- ✅ `export` - Bisa export
## 🏗️ Arsitektur Sistem
### Opsi 1: Menu-Action Based (Recommended) ⭐
**Konsep:**
- Menu memiliki Actions langsung (bukan melalui Module)
- User Level memiliki akses ke Menu
- User Level memiliki akses ke Actions di dalam Menu
**Struktur Database:**
#### 1. `master_menus` (Existing - Enhanced)
```sql
-- Sudah ada, tidak perlu perubahan
-- Menu seperti: "Content Management", "User Management", "Settings"
```
#### 2. `menu_actions` (New)
```sql
CREATE TABLE menu_actions (
id SERIAL PRIMARY KEY,
menu_id INT NOT NULL,
action_code VARCHAR(50) NOT NULL, -- 'view', 'create', 'edit', 'delete', 'approve', 'export'
action_name VARCHAR(255) NOT NULL, -- 'View Content', 'Create Content', etc.
description TEXT NULL,
path_url VARCHAR(255) NULL, -- Optional: untuk routing frontend
http_method VARCHAR(10) NULL, -- Optional: 'GET', 'POST', 'PUT', 'DELETE'
position INT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (menu_id) REFERENCES master_menus(id) ON DELETE CASCADE,
UNIQUE(menu_id, action_code)
);
CREATE INDEX idx_menu_actions_menu_id ON menu_actions(menu_id);
CREATE INDEX idx_menu_actions_action_code ON menu_actions(action_code);
```
#### 3. `user_level_menu_accesses` (New)
```sql
CREATE TABLE user_level_menu_accesses (
id SERIAL PRIMARY KEY,
user_level_id INT NOT NULL,
menu_id INT NOT NULL,
can_access BOOLEAN DEFAULT TRUE,
client_id UUID NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (user_level_id) REFERENCES user_levels(id) ON DELETE CASCADE,
FOREIGN KEY (menu_id) REFERENCES master_menus(id) ON DELETE CASCADE,
UNIQUE(user_level_id, menu_id, client_id)
);
CREATE INDEX idx_user_level_menu_accesses_user_level_id ON user_level_menu_accesses(user_level_id);
CREATE INDEX idx_user_level_menu_accesses_menu_id ON user_level_menu_accesses(menu_id);
```
#### 4. `user_level_menu_action_accesses` (New)
```sql
CREATE TABLE user_level_menu_action_accesses (
id SERIAL PRIMARY KEY,
user_level_id INT NOT NULL,
menu_id INT NOT NULL,
action_code VARCHAR(50) NOT NULL,
can_access BOOLEAN DEFAULT TRUE,
client_id UUID NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (user_level_id) REFERENCES user_levels(id) ON DELETE CASCADE,
FOREIGN KEY (menu_id) REFERENCES master_menus(id) ON DELETE CASCADE,
FOREIGN KEY (menu_id, action_code) REFERENCES menu_actions(menu_id, action_code) ON DELETE CASCADE,
UNIQUE(user_level_id, menu_id, action_code, client_id)
);
CREATE INDEX idx_user_level_menu_action_accesses_user_level_id ON user_level_menu_action_accesses(user_level_id);
CREATE INDEX idx_user_level_menu_action_accesses_menu_id ON user_level_menu_action_accesses(menu_id);
CREATE INDEX idx_user_level_menu_action_accesses_action_code ON user_level_menu_action_accesses(action_code);
```
**Relasi:**
```
master_menus (1) ─────< (N) menu_actions
│ │
│ │
│ (1) │ (N)
│ │
user_level_menu_accesses user_level_menu_action_accesses
│ │
│ (N) │ (N)
│ │
user_levels (1) ──────────────┘
```
**Keuntungan:**
- ✅ Lebih sederhana dan intuitif
- ✅ Langsung ke point: Menu → Actions
- ✅ Mudah di-manage: assign menu, lalu assign actions
- ✅ Tidak perlu konsep "Module" yang membingungkan
- ✅ Frontend lebih mudah: cek menu access, lalu cek action access
**Kekurangan:**
- ❌ Perlu migration dari sistem Module yang ada
- ❌ Perlu refactor middleware dan service yang sudah ada
---
### Opsi 2: Keep Module, Enhance Structure
**Konsep:**
- Tetap menggunakan Module, tapi Module sekarang lebih spesifik ke Menu
- Module = Action di dalam Menu
- User Level memiliki akses ke Menu
- User Level memiliki akses ke Module (Action) di dalam Menu
**Struktur Database:**
#### 1. `master_menus` (Existing)
```sql
-- Tidak berubah
```
#### 2. `master_modules` (Existing - Enhanced)
```sql
-- Tetap ada, tapi sekarang lebih spesifik:
-- Module sekarang = Action di dalam Menu
-- action_type menjadi lebih penting
```
#### 3. `menu_modules` (Existing - Enhanced)
```sql
-- Tetap ada, relasi Menu dengan Module (Action)
-- Tapi sekarang lebih strict: 1 module hanya untuk 1 menu
```
#### 4. `user_level_menu_accesses` (New)
```sql
-- Sama seperti Opsi 1
```
#### 5. `user_level_module_accesses` (Existing)
```sql
-- Tetap ada, tapi sekarang lebih spesifik:
-- Module = Action di dalam Menu
```
**Keuntungan:**
- ✅ Tidak perlu migration besar-besaran
- ✅ Bisa reuse struktur yang sudah ada
- ✅ Lebih fleksibel (module bisa digunakan oleh banyak menu)
**Kekurangan:**
- ❌ Konsep "Module" masih membingungkan
- ❌ Lebih kompleks: Menu → Module → Action
- ❌ Perlu mapping yang lebih kompleks
---
## 🎯 Rekomendasi: Opsi 1 (Menu-Action Based)
**Alasan:**
1. **Lebih intuitif** - Langsung ke point: Menu punya Actions
2. **Lebih mudah di-manage** - Admin langsung assign menu dan actions
3. **Lebih mudah di-frontend** - Cek menu access, lalu cek action access
4. **Lebih scalable** - Mudah ditambah menu baru dengan actions baru
5. **Lebih maintainable** - Struktur lebih jelas dan mudah dipahami
## 📝 Implementation Plan
### Phase 1: Database Schema
1. ✅ Buat tabel `menu_actions`
2. ✅ Buat tabel `user_level_menu_accesses`
3. ✅ Buat tabel `user_level_menu_action_accesses`
4. ✅ Buat migration script
5. ✅ Migrate data dari `master_modules` dan `menu_modules` ke `menu_actions` (jika perlu)
### Phase 2: Backend Entities & Repositories
1. ✅ Buat entity `MenuActions`
2. ✅ Buat entity `UserLevelMenuAccesses`
3. ✅ Buat entity `UserLevelMenuActionAccesses`
4. ✅ Buat repository untuk masing-masing entity
5. ✅ Buat service untuk masing-masing entity
### Phase 3: Backend API Endpoints
1. ✅ CRUD API untuk `menu_actions`
2. ✅ CRUD API untuk `user_level_menu_accesses`
3. ✅ CRUD API untuk `user_level_menu_action_accesses`
4. ✅ API untuk get menu actions by menu_id
5. ✅ API untuk get user level menu accesses
6. ✅ API untuk get user level menu action accesses
### Phase 4: Backend Middleware
1. ✅ Buat middleware `CheckMenuAccess` - cek apakah user level bisa akses menu
2. ✅ Buat middleware `CheckMenuActionAccess` - cek apakah user level bisa akses action di menu
3. ✅ Update existing middleware untuk menggunakan struktur baru
### Phase 5: Frontend Services
1. ✅ Buat service untuk `menu_actions`
2. ✅ Buat service untuk `user_level_menu_accesses`
3. ✅ Buat service untuk `user_level_menu_action_accesses`
4. ✅ Update existing services
### Phase 6: Frontend UI - Menu Management
1. ✅ Update halaman Menu Management untuk menampilkan Actions
2. ✅ Tambah form untuk manage Actions di setiap Menu
3. ✅ Tambah UI untuk assign Actions ke Menu
### Phase 7: Frontend UI - User Level Management
1. ✅ Update halaman User Level Management
2. ✅ Tambah tab "Menu Access" untuk assign menu ke user level
3. ✅ Tambah tab "Action Access" untuk assign actions per menu ke user level
4. ✅ Update form User Level untuk include menu dan action access
### Phase 8: Frontend UI - Permission Check
1. ✅ Buat hook `useMenuAccess` untuk cek menu access
2. ✅ Buat hook `useMenuActionAccess` untuk cek action access
3. ✅ Update components untuk menggunakan hooks
4. ✅ Hide/show UI elements berdasarkan permission
### Phase 9: Testing & Documentation
1. ✅ Test semua API endpoints
2. ✅ Test middleware
3. ✅ Test frontend UI
4. ✅ Update documentation
## 🔄 Migration Strategy
### Option A: Clean Slate (Recommended)
- Buat struktur baru dari awal
- Data lama tetap ada tapi tidak digunakan
- Migrate data secara bertahap jika perlu
### Option B: Gradual Migration
- Keep struktur lama dan baru berjalan bersamaan
- Migrate data dari lama ke baru
- Deprecate struktur lama setelah semua ter-migrate
## 📊 Comparison: Current vs Proposed
### Current System
```
Menu → Module (via menu_modules) → User Level Access (via user_level_module_accesses)
```
- Module adalah entitas terpisah
- Module bisa digunakan oleh banyak menu
- Akses diberikan ke Module, bukan ke Menu + Action
### Proposed System
```
Menu → Actions (via menu_actions) → User Level Menu Access → User Level Action Access
```
- Actions langsung di dalam Menu
- Actions spesifik untuk Menu tertentu
- Akses diberikan ke Menu dan Actions secara terpisah
## ✅ Next Steps
1. **Review & Approval** - Review plan ini dengan tim
2. **Decision** - Pilih antara Opsi 1 atau Opsi 2
3. **Database Design** - Finalize database schema
4. **Implementation** - Mulai implementasi sesuai phase yang sudah direncanakan
## 📚 Reference
- Current system: `docs/MENU_MODULE_ACCESS_SYSTEM.md`
- Implementation summary: `docs/IMPLEMENTATION_SUMMARY_MENU_MODULE_ACCESS.md`

View File

@ -0,0 +1,195 @@
-- Migration: Add Menu Action Access System
-- Description: Create menu_actions, user_level_menu_accesses, and user_level_menu_action_accesses tables
-- Date: 2026-01-16
-- ============================================================================
-- Step 1: Create menu_actions table
-- ============================================================================
CREATE TABLE IF NOT EXISTS menu_actions (
id SERIAL PRIMARY KEY,
menu_id INT NOT NULL,
action_code VARCHAR(50) NOT NULL,
action_name VARCHAR(255) NOT NULL,
description TEXT NULL,
path_url VARCHAR(255) NULL,
http_method VARCHAR(10) NULL,
position INT NULL,
client_id UUID NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (menu_id) REFERENCES master_menus(id) ON DELETE CASCADE,
UNIQUE(menu_id, action_code, COALESCE(client_id, '00000000-0000-0000-0000-000000000000'::UUID))
);
-- Add indexes for performance
CREATE INDEX idx_menu_actions_menu_id ON menu_actions(menu_id);
CREATE INDEX idx_menu_actions_action_code ON menu_actions(action_code);
CREATE INDEX idx_menu_actions_client_id ON menu_actions(client_id) WHERE client_id IS NOT NULL;
CREATE INDEX idx_menu_actions_is_active ON menu_actions(is_active);
-- Add comments
COMMENT ON TABLE menu_actions IS 'Actions yang tersedia di setiap menu (view, create, edit, delete, approve, export, etc)';
COMMENT ON COLUMN menu_actions.menu_id IS 'Foreign key ke master_menus';
COMMENT ON COLUMN menu_actions.action_code IS 'Kode action: view, create, edit, delete, approve, export, etc';
COMMENT ON COLUMN menu_actions.action_name IS 'Nama action yang ditampilkan ke user';
COMMENT ON COLUMN menu_actions.path_url IS 'Optional: URL path untuk routing frontend';
COMMENT ON COLUMN menu_actions.http_method IS 'Optional: HTTP method (GET, POST, PUT, DELETE)';
-- ============================================================================
-- Step 2: Create user_level_menu_accesses table
-- ============================================================================
CREATE TABLE IF NOT EXISTS user_level_menu_accesses (
id SERIAL PRIMARY KEY,
user_level_id INT NOT NULL,
menu_id INT NOT NULL,
can_access BOOLEAN DEFAULT TRUE,
client_id UUID NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (user_level_id) REFERENCES user_levels(id) ON DELETE CASCADE,
FOREIGN KEY (menu_id) REFERENCES master_menus(id) ON DELETE CASCADE,
UNIQUE(user_level_id, menu_id, COALESCE(client_id, '00000000-0000-0000-0000-000000000000'::UUID))
);
-- Add indexes for performance
CREATE INDEX idx_user_level_menu_accesses_user_level_id ON user_level_menu_accesses(user_level_id);
CREATE INDEX idx_user_level_menu_accesses_menu_id ON user_level_menu_accesses(menu_id);
CREATE INDEX idx_user_level_menu_accesses_client_id ON user_level_menu_accesses(client_id) WHERE client_id IS NOT NULL;
CREATE INDEX idx_user_level_menu_accesses_is_active ON user_level_menu_accesses(is_active);
-- Add comments
COMMENT ON TABLE user_level_menu_accesses IS 'Mengatur akses user_level ke menu tertentu';
COMMENT ON COLUMN user_level_menu_accesses.user_level_id IS 'Foreign key ke user_levels';
COMMENT ON COLUMN user_level_menu_accesses.menu_id IS 'Foreign key ke master_menus';
COMMENT ON COLUMN user_level_menu_accesses.can_access IS 'Apakah user level boleh mengakses menu ini';
-- ============================================================================
-- Step 3: Create user_level_menu_action_accesses table
-- ============================================================================
CREATE TABLE IF NOT EXISTS user_level_menu_action_accesses (
id SERIAL PRIMARY KEY,
user_level_id INT NOT NULL,
menu_id INT NOT NULL,
action_code VARCHAR(50) NOT NULL,
can_access BOOLEAN DEFAULT TRUE,
client_id UUID NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
FOREIGN KEY (user_level_id) REFERENCES user_levels(id) ON DELETE CASCADE,
FOREIGN KEY (menu_id) REFERENCES master_menus(id) ON DELETE CASCADE,
UNIQUE(user_level_id, menu_id, action_code, COALESCE(client_id, '00000000-0000-0000-0000-000000000000'::UUID))
);
-- Add indexes for performance
CREATE INDEX idx_user_level_menu_action_accesses_user_level_id ON user_level_menu_action_accesses(user_level_id);
CREATE INDEX idx_user_level_menu_action_accesses_menu_id ON user_level_menu_action_accesses(menu_id);
CREATE INDEX idx_user_level_menu_action_accesses_action_code ON user_level_menu_action_accesses(action_code);
CREATE INDEX idx_user_level_menu_action_accesses_client_id ON user_level_menu_action_accesses(client_id) WHERE client_id IS NOT NULL;
CREATE INDEX idx_user_level_menu_action_accesses_is_active ON user_level_menu_action_accesses(is_active);
-- Add comments
COMMENT ON TABLE user_level_menu_action_accesses IS 'Mengatur akses user_level ke action tertentu di dalam menu';
COMMENT ON COLUMN user_level_menu_action_accesses.user_level_id IS 'Foreign key ke user_levels';
COMMENT ON COLUMN user_level_menu_action_accesses.menu_id IS 'Foreign key ke master_menus';
COMMENT ON COLUMN user_level_menu_action_accesses.action_code IS 'Kode action (harus sesuai dengan menu_actions.action_code)';
COMMENT ON COLUMN user_level_menu_action_accesses.can_access IS 'Apakah user level boleh melakukan action ini';
-- ============================================================================
-- Step 4: Create trigger for updated_at
-- ============================================================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply trigger to menu_actions
DROP TRIGGER IF EXISTS update_menu_actions_updated_at ON menu_actions;
CREATE TRIGGER update_menu_actions_updated_at
BEFORE UPDATE ON menu_actions
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Apply trigger to user_level_menu_accesses
DROP TRIGGER IF EXISTS update_user_level_menu_accesses_updated_at ON user_level_menu_accesses;
CREATE TRIGGER update_user_level_menu_accesses_updated_at
BEFORE UPDATE ON user_level_menu_accesses
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Apply trigger to user_level_menu_action_accesses
DROP TRIGGER IF EXISTS update_user_level_menu_action_accesses_updated_at ON user_level_menu_action_accesses;
CREATE TRIGGER update_user_level_menu_action_accesses_updated_at
BEFORE UPDATE ON user_level_menu_action_accesses
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- ============================================================================
-- Verification
-- ============================================================================
DO $$
DECLARE
v_menu_actions_exists BOOLEAN;
v_user_level_menu_accesses_exists BOOLEAN;
v_user_level_menu_action_accesses_exists BOOLEAN;
BEGIN
-- Check menu_actions table
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'menu_actions'
) INTO v_menu_actions_exists;
-- Check user_level_menu_accesses table
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'user_level_menu_accesses'
) INTO v_user_level_menu_accesses_exists;
-- Check user_level_menu_action_accesses table
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'user_level_menu_action_accesses'
) INTO v_user_level_menu_action_accesses_exists;
-- Report results
RAISE NOTICE '========================================';
RAISE NOTICE 'Migration Verification Results:';
RAISE NOTICE '========================================';
IF v_menu_actions_exists THEN
RAISE NOTICE '✓ Table menu_actions created successfully';
ELSE
RAISE WARNING '✗ Table menu_actions NOT created';
END IF;
IF v_user_level_menu_accesses_exists THEN
RAISE NOTICE '✓ Table user_level_menu_accesses created successfully';
ELSE
RAISE WARNING '✗ Table user_level_menu_accesses NOT created';
END IF;
IF v_user_level_menu_action_accesses_exists THEN
RAISE NOTICE '✓ Table user_level_menu_action_accesses created successfully';
ELSE
RAISE WARNING '✗ Table user_level_menu_action_accesses NOT created';
END IF;
RAISE NOTICE '========================================';
IF v_menu_actions_exists AND v_user_level_menu_accesses_exists AND v_user_level_menu_action_accesses_exists THEN
RAISE NOTICE 'Migration completed successfully! ✓';
ELSE
RAISE WARNING 'Migration completed with errors! Please check above.';
END IF;
RAISE NOTICE '========================================';
END $$;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,9 @@ import (
"netidhub-saas-be/app/module/magazines"
"netidhub-saas-be/app/module/master_menus"
"netidhub-saas-be/app/module/master_modules"
"netidhub-saas-be/app/module/menu_actions"
"netidhub-saas-be/app/module/user_level_menu_accesses"
"netidhub-saas-be/app/module/user_level_menu_action_accesses"
"netidhub-saas-be/app/module/provinces"
"netidhub-saas-be/app/module/schedules"
"netidhub-saas-be/app/module/subscription"
@ -93,6 +96,9 @@ func main() {
magazine_files.NewMagazineFilesModule,
master_menus.NewMasterMenusModule,
master_modules.NewMasterModulesModule,
menu_actions.NewMenuActionsModule,
user_level_menu_accesses.NewUserLevelMenuAccessesModule,
user_level_menu_action_accesses.NewUserLevelMenuActionAccessesModule,
provinces.NewProvincesModule,
schedules.NewSchedulesModule,
subscription.NewSubscriptionModule,