feat: update menu and module modulars

This commit is contained in:
hanif salafi 2026-01-15 16:04:49 +07:00
parent 75b8edb124
commit 1a9ec470b2
25 changed files with 3382 additions and 0 deletions

View File

@ -10,6 +10,7 @@ type MasterModules struct {
Name string `json:"name" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
PathUrl string `json:"path_url" gorm:"type:varchar"`
ActionType *string `json:"action_type" gorm:"type:varchar"` // view, create, edit, delete, approve, export, etc
StatusId int `json:"status_id" gorm:"type:int4"`
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`

View File

@ -0,0 +1,24 @@
package entity
import (
"github.com/google/uuid"
"time"
)
// MenuModules menghubungkan menu dengan modul-modul yang dimilikinya
// Contoh: Menu "Article" bisa punya modul: "table_article", "create_article", "edit_article", "delete_article"
type MenuModules struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
MenuId uint `json:"menu_id" gorm:"type:int4;not null"`
ModuleId uint `json:"module_id" gorm:"type:int4;not null"`
Position *int `json:"position" gorm:"type:int4"` // Urutan modul dalam menu
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"`
Module *MasterModules `json:"module,omitempty" gorm:"foreignKey:ModuleId"`
}

View File

@ -0,0 +1,24 @@
package entity
import (
"github.com/google/uuid"
"time"
)
// UserLevelModuleAccesses mengatur akses user_level ke modul-modul tertentu
// Contoh: UserLevel "Admin Pusat" bisa akses semua modul, "Editor" hanya bisa akses "create_article" dan "edit_article"
type UserLevelModuleAccesses struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserLevelId uint `json:"user_level_id" gorm:"type:int4;not null"`
ModuleId uint `json:"module_id" gorm:"type:int4;not null"`
CanAccess bool `json:"can_access" gorm:"type:bool;default:true"` // Apakah boleh akses modul 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"`
Module *MasterModules `json:"module,omitempty" gorm:"foreignKey:ModuleId"`
}

View File

@ -0,0 +1,295 @@
package middleware
import (
"netidhub-saas-be/app/database"
"netidhub-saas-be/app/database/entity"
"github.com/gofiber/fiber/v2"
)
type ModuleAccessMiddleware struct {
DB *database.Database
}
func NewModuleAccessMiddleware(db *database.Database) *ModuleAccessMiddleware {
return &ModuleAccessMiddleware{
DB: db,
}
}
// CheckModuleAccess middleware untuk validasi akses user_level ke modul tertentu
// Menggunakan module_id atau path_url sebagai identifier
func (m *ModuleAccessMiddleware) CheckModuleAccess(moduleIdentifier interface{}) fiber.Handler {
return func(c *fiber.Ctx) error {
// Get user from context (diasumsikan sudah ada middleware auth yang set user ke 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"},
})
}
// Cast user dari context
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
// Dapatkan module berdasarkan identifier (bisa module_id atau path_url)
var module entity.MasterModules
var err error
switch v := moduleIdentifier.(type) {
case uint:
// Jika moduleIdentifier adalah ID
err = m.DB.DB.Where("id = ? AND is_active = ?", v, true).First(&module).Error
case string:
// Jika moduleIdentifier adalah path_url
err = m.DB.DB.Where("path_url = ? AND is_active = ?", v, true).First(&module).Error
default:
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"success": false,
"code": 400,
"messages": []string{"Module identifier tidak valid"},
})
}
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"code": 404,
"messages": []string{"Module tidak ditemukan"},
})
}
// Check akses user_level ke module
var access entity.UserLevelModuleAccesses
err = m.DB.DB.Where(
"user_level_id = ? AND module_id = ? AND is_active = ?",
userLevelId,
module.ID,
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 modul ini"},
"user_level_id": userLevelId,
"module_id": module.ID,
"module_name": module.Name,
})
}
if !access.CanAccess {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"success": false,
"code": 403,
"messages": []string{"Akses ke modul ini ditolak"},
"user_level_id": userLevelId,
"module_id": module.ID,
"module_name": module.Name,
})
}
// Set module ke context untuk digunakan di handler
c.Locals("module", &module)
c.Locals("user_level_id", userLevelId)
return c.Next()
}
}
// CheckModuleAccessByPath middleware untuk validasi akses berdasarkan path yang sedang diakses
// Akan otomatis mencocokkan path dengan module.path_url
func (m *ModuleAccessMiddleware) CheckModuleAccessByPath() 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
currentPath := c.Path()
// Cari module berdasarkan path_url yang cocok
var module entity.MasterModules
err := m.DB.DB.Where("path_url = ? AND is_active = ?", currentPath, true).First(&module).Error
if err != nil {
// Jika module tidak ditemukan, bisa jadi path ini tidak perlu validasi modul
// Atau bisa langsung return error tergantung kebijakan
return c.Next() // Skip validation jika module tidak ditemukan
}
// Check akses user_level ke module
var access entity.UserLevelModuleAccesses
err = m.DB.DB.Where(
"user_level_id = ? AND module_id = ? AND is_active = ?",
userLevelId,
module.ID,
true,
).First(&access).Error
if err != nil || !access.CanAccess {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
"success": false,
"code": 403,
"messages": []string{"Anda tidak memiliki akses ke halaman ini"},
"user_level_id": userLevelId,
"path": currentPath,
"module_name": module.Name,
})
}
// Set module ke context
c.Locals("module", &module)
c.Locals("user_level_id", userLevelId)
return c.Next()
}
}
// CheckMenuAccess middleware untuk validasi akses user_level ke menu beserta modul-modulnya
func (m *ModuleAccessMiddleware) 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
// Get menu
var menu entity.MasterMenus
if err := m.DB.DB.Where("id = ? AND is_active = ?", menuId, true).First(&menu).Error; err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"success": false,
"code": 404,
"messages": []string{"Menu tidak ditemukan"},
})
}
// Get semua modul yang ada di menu ini
var menuModules []entity.MenuModules
if err := m.DB.DB.Where("menu_id = ? AND is_active = ?", menuId, true).Find(&menuModules).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"code": 500,
"messages": []string{"Error mendapatkan menu modules"},
"error": err.Error(),
})
}
if len(menuModules) == 0 {
// Jika menu tidak punya modul, skip validasi
return c.Next()
}
// Check apakah user_level memiliki akses ke minimal satu modul di menu ini
hasAccess := false
for _, menuModule := range menuModules {
var access entity.UserLevelModuleAccesses
err := m.DB.DB.Where(
"user_level_id = ? AND module_id = ? AND is_active = ? AND can_access = ?",
userLevelId,
menuModule.ModuleId,
true,
true,
).First(&access).Error
if err == nil {
hasAccess = true
break
}
}
if !hasAccess {
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,
"menu_name": menu.Name,
})
}
// Set menu ke context
c.Locals("menu", &menu)
c.Locals("user_level_id", userLevelId)
return c.Next()
}
}

View File

@ -0,0 +1,14 @@
package controller
import "netidhub-saas-be/app/module/menu_modules/service"
type Controller struct {
MenuModules MenuModulesController
}
func NewController(menuModulesService service.MenuModulesService) *Controller {
return &Controller{
MenuModules: NewMenuModulesController(menuModulesService),
}
}

View File

@ -0,0 +1,293 @@
package controller
import (
"netidhub-saas-be/app/module/menu_modules/request"
"netidhub-saas-be/app/module/menu_modules/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 menuModulesController struct {
menuModulesService service.MenuModulesService
}
type MenuModulesController interface {
All(c *fiber.Ctx) error
GetByMenuId(c *fiber.Ctx) error
GetByModuleId(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 NewMenuModulesController(menuModulesService service.MenuModulesService) MenuModulesController {
return &menuModulesController{
menuModulesService: menuModulesService,
}
}
// All MenuModules
// @Summary Get all MenuModules
// @Description API for getting all MenuModules
// @Tags MenuModules
// @Security Bearer
// @Param menu_id query int false "Menu ID"
// @Param module_id query int false "Module 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 /menu-modules [get]
func (_i *menuModulesController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
req := request.MenuModulesQueryRequest{
Pagination: paginate,
}
if menuId := c.Query("menu_id"); menuId != "" {
id, _ := strconv.ParseUint(menuId, 10, 0)
menuIdUint := uint(id)
req.MenuId = &menuIdUint
}
if moduleId := c.Query("module_id"); moduleId != "" {
id, _ := strconv.ParseUint(moduleId, 10, 0)
moduleIdUint := uint(id)
req.ModuleId = &moduleIdUint
}
menuModulesData, paging, err := _i.menuModulesService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuModules list successfully retrieved"},
Data: menuModulesData,
Meta: paging,
})
}
// GetByMenuId get MenuModules by Menu ID
// @Summary Get MenuModules by Menu ID
// @Description API for getting MenuModules by Menu ID
// @Tags MenuModules
// @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-modules/menu/{menu_id} [get]
func (_i *menuModulesController) GetByMenuId(c *fiber.Ctx) error {
menuId, err := strconv.ParseUint(c.Params("menu_id"), 10, 0)
if err != nil {
return err
}
menuModulesData, err := _i.menuModulesService.GetByMenuId(uint(menuId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuModules by menu successfully retrieved"},
Data: menuModulesData,
})
}
// GetByModuleId get MenuModules by Module ID
// @Summary Get MenuModules by Module ID
// @Description API for getting MenuModules by Module ID
// @Tags MenuModules
// @Security Bearer
// @Param module_id path int true "Module ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /menu-modules/module/{module_id} [get]
func (_i *menuModulesController) GetByModuleId(c *fiber.Ctx) error {
moduleId, err := strconv.ParseUint(c.Params("module_id"), 10, 0)
if err != nil {
return err
}
menuModulesData, err := _i.menuModulesService.GetByModuleId(uint(moduleId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuModules by module successfully retrieved"},
Data: menuModulesData,
})
}
// Show get one MenuModule
// @Summary Get one MenuModule
// @Description API for getting one MenuModule
// @Tags MenuModules
// @Security Bearer
// @Param id path int true "MenuModule ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /menu-modules/{id} [get]
func (_i *menuModulesController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
menuModuleData, err := _i.menuModulesService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuModule successfully retrieved"},
Data: menuModuleData,
})
}
// Save create MenuModule
// @Summary Create MenuModule
// @Description API for create MenuModule
// @Tags MenuModules
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.MenuModulesCreateRequest 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-modules [post]
func (_i *menuModulesController) Save(c *fiber.Ctx) error {
req := new(request.MenuModulesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.menuModulesService.Save(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuModule successfully created"},
})
}
// SaveBatch create multiple MenuModules at once
// @Summary Create multiple MenuModules
// @Description API for creating multiple MenuModules at once for a menu
// @Tags MenuModules
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.MenuModulesBatchCreateRequest 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-modules/batch [post]
func (_i *menuModulesController) SaveBatch(c *fiber.Ctx) error {
req := new(request.MenuModulesBatchCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.menuModulesService.SaveBatch(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuModules successfully created in batch"},
})
}
// Update MenuModule
// @Summary Update MenuModule
// @Description API for update MenuModule
// @Tags MenuModules
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.MenuModulesUpdateRequest true "Required payload"
// @Param id path int true "MenuModule ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /menu-modules/{id} [put]
func (_i *menuModulesController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.MenuModulesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.menuModulesService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuModule successfully updated"},
})
}
// Delete MenuModule
// @Summary Delete MenuModule
// @Description API for delete MenuModule (soft delete)
// @Tags MenuModules
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param id path int true "MenuModule ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /menu-modules/{id} [delete]
func (_i *menuModulesController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.menuModulesService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"MenuModule successfully deleted"},
})
}

View File

@ -0,0 +1,42 @@
package mapper
import (
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/menu_modules/response"
)
func MenuModulesResponseMapper(e *entity.MenuModules) *response.MenuModulesResponse {
resp := &response.MenuModulesResponse{
ID: e.ID,
MenuId: e.MenuId,
ModuleId: e.ModuleId,
Position: e.Position,
ClientId: e.ClientId,
IsActive: e.IsActive,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
if e.Menu != nil {
resp.Menu = &response.MenuBasicResponse{
ID: e.Menu.ID,
Name: e.Menu.Name,
Description: e.Menu.Description,
Icon: e.Menu.Icon,
Group: e.Menu.Group,
}
}
if e.Module != nil {
resp.Module = &response.ModuleBasicResponse{
ID: e.Module.ID,
Name: e.Module.Name,
Description: e.Module.Description,
PathUrl: e.Module.PathUrl,
ActionType: e.Module.ActionType,
}
}
return resp
}

View File

@ -0,0 +1,57 @@
package menu_modules
import (
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
"netidhub-saas-be/app/module/menu_modules/controller"
"netidhub-saas-be/app/module/menu_modules/repository"
"netidhub-saas-be/app/module/menu_modules/service"
)
// struct of MenuModulesRouter
type MenuModulesRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of MenuModules module
var NewMenuModulesModule = fx.Options(
// register repository of MenuModules module
fx.Provide(repository.NewMenuModulesRepository),
// register service of MenuModules module
fx.Provide(service.NewMenuModulesService),
// register controller of MenuModules module
fx.Provide(controller.NewController),
// register router of MenuModules module
fx.Provide(NewMenuModulesRouter),
)
// init MenuModulesRouter
func NewMenuModulesRouter(fiber *fiber.App, controller *controller.Controller) *MenuModulesRouter {
return &MenuModulesRouter{
App: fiber,
Controller: controller,
}
}
// register routes of MenuModules module
func (_i *MenuModulesRouter) RegisterMenuModulesRoutes() {
// define controllers
menuModulesController := _i.Controller.MenuModules
// define routes
_i.App.Route("/menu-modules", func(router fiber.Router) {
router.Get("/", menuModulesController.All)
router.Get("/:id", menuModulesController.Show)
router.Get("/menu/:menu_id", menuModulesController.GetByMenuId)
router.Get("/module/:module_id", menuModulesController.GetByModuleId)
router.Post("/", menuModulesController.Save)
router.Post("/batch", menuModulesController.SaveBatch)
router.Put("/:id", menuModulesController.Update)
router.Delete("/:id", menuModulesController.Delete)
})
}

View File

@ -0,0 +1,130 @@
package repository
import (
"fmt"
"netidhub-saas-be/app/database"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/menu_modules/request"
"netidhub-saas-be/utils/paginator"
)
type menuModulesRepository struct {
DB *database.Database
}
// MenuModulesRepository define interface of IMenuModulesRepository
type MenuModulesRepository interface {
GetAll(req request.MenuModulesQueryRequest) (menuModules []*entity.MenuModules, paging paginator.Pagination, err error)
GetByMenuId(menuId uint) (menuModules []*entity.MenuModules, err error)
GetByModuleId(moduleId uint) (menuModules []*entity.MenuModules, err error)
FindOne(id uint) (menuModule *entity.MenuModules, err error)
Create(menuModule *entity.MenuModules) (err error)
CreateBatch(menuModules []*entity.MenuModules) (err error)
Update(id uint, menuModule *entity.MenuModules) (err error)
Delete(id uint) (err error)
DeleteByMenuId(menuId uint) (err error)
DeleteByModuleId(moduleId uint) (err error)
}
func NewMenuModulesRepository(db *database.Database) MenuModulesRepository {
return &menuModulesRepository{
DB: db,
}
}
// implement interface of IMenuModulesRepository
func (_i *menuModulesRepository) GetAll(req request.MenuModulesQueryRequest) (menuModules []*entity.MenuModules, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.MenuModules{})
query = query.Where("is_active = ?", true)
if req.MenuId != nil {
query = query.Where("menu_id = ?", req.MenuId)
}
if req.ModuleId != nil {
query = query.Where("module_id = ?", req.ModuleId)
}
if req.ClientId != nil {
query = query.Where("client_id = ?", req.ClientId)
}
// Preload relations
query = query.Preload("Menu").Preload("Module")
query.Count(&count)
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")
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&menuModules).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *menuModulesRepository) GetByMenuId(menuId uint) (menuModules []*entity.MenuModules, err error) {
query := _i.DB.DB.Model(&entity.MenuModules{})
query = query.Where("menu_id = ? AND is_active = ?", menuId, true)
query = query.Preload("Module")
query = query.Order("position ASC")
err = query.Find(&menuModules).Error
return
}
func (_i *menuModulesRepository) GetByModuleId(moduleId uint) (menuModules []*entity.MenuModules, err error) {
query := _i.DB.DB.Model(&entity.MenuModules{})
query = query.Where("module_id = ? AND is_active = ?", moduleId, true)
query = query.Preload("Menu")
err = query.Find(&menuModules).Error
return
}
func (_i *menuModulesRepository) FindOne(id uint) (menuModule *entity.MenuModules, err error) {
query := _i.DB.DB.Preload("Menu").Preload("Module")
if err := query.First(&menuModule, id).Error; err != nil {
return nil, err
}
return menuModule, nil
}
func (_i *menuModulesRepository) Create(menuModule *entity.MenuModules) (err error) {
return _i.DB.DB.Create(menuModule).Error
}
func (_i *menuModulesRepository) CreateBatch(menuModules []*entity.MenuModules) (err error) {
return _i.DB.DB.Create(&menuModules).Error
}
func (_i *menuModulesRepository) Update(id uint, menuModule *entity.MenuModules) (err error) {
return _i.DB.DB.Model(&entity.MenuModules{}).
Where(&entity.MenuModules{ID: id}).
Updates(menuModule).Error
}
func (_i *menuModulesRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.MenuModules{}, id).Error
}
func (_i *menuModulesRepository) DeleteByMenuId(menuId uint) (err error) {
return _i.DB.DB.Where("menu_id = ?", menuId).Delete(&entity.MenuModules{}).Error
}
func (_i *menuModulesRepository) DeleteByModuleId(moduleId uint) (err error) {
return _i.DB.DB.Where("module_id = ?", moduleId).Delete(&entity.MenuModules{}).Error
}

View File

@ -0,0 +1,36 @@
package request
import (
"github.com/google/uuid"
"netidhub-saas-be/utils/paginator"
)
type MenuModulesQueryRequest struct {
MenuId *uint `query:"menu_id"`
ModuleId *uint `query:"module_id"`
ClientId *uuid.UUID `query:"client_id"`
Pagination *paginator.Query `query:"pagination"`
}
type MenuModulesCreateRequest struct {
MenuId uint `json:"menu_id" validate:"required"`
ModuleId uint `json:"module_id" validate:"required"`
Position *int `json:"position"`
ClientId *uuid.UUID `json:"client_id"`
IsActive *bool `json:"is_active"`
}
type MenuModulesBatchCreateRequest struct {
MenuId uint `json:"menu_id" validate:"required"`
ModuleIds []uint `json:"module_ids" validate:"required,min=1"`
ClientId *uuid.UUID `json:"client_id"`
}
type MenuModulesUpdateRequest struct {
MenuId *uint `json:"menu_id"`
ModuleId *uint `json:"module_id"`
Position *int `json:"position"`
ClientId *uuid.UUID `json:"client_id"`
IsActive *bool `json:"is_active"`
}

View File

@ -0,0 +1,36 @@
package response
import (
"github.com/google/uuid"
"time"
)
type MenuModulesResponse struct {
ID uint `json:"id"`
MenuId uint `json:"menu_id"`
ModuleId uint `json:"module_id"`
Position *int `json:"position"`
ClientId *uuid.UUID `json:"client_id"`
IsActive *bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Menu *MenuBasicResponse `json:"menu,omitempty"`
Module *ModuleBasicResponse `json:"module,omitempty"`
}
type MenuBasicResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Icon *string `json:"icon"`
Group string `json:"group"`
}
type ModuleBasicResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
PathUrl string `json:"path_url"`
ActionType *string `json:"action_type"`
}

View File

@ -0,0 +1,161 @@
package service
import (
"github.com/rs/zerolog"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/menu_modules/mapper"
"netidhub-saas-be/app/module/menu_modules/repository"
"netidhub-saas-be/app/module/menu_modules/request"
"netidhub-saas-be/app/module/menu_modules/response"
"netidhub-saas-be/utils/paginator"
)
// MenuModulesService
type menuModulesService struct {
Repo repository.MenuModulesRepository
Log zerolog.Logger
}
// MenuModulesService define interface of IMenuModulesService
type MenuModulesService interface {
All(req request.MenuModulesQueryRequest) (menuModules []*response.MenuModulesResponse, paging paginator.Pagination, err error)
GetByMenuId(menuId uint) (menuModules []*response.MenuModulesResponse, err error)
GetByModuleId(moduleId uint) (menuModules []*response.MenuModulesResponse, err error)
Show(id uint) (menuModule *response.MenuModulesResponse, err error)
Save(req request.MenuModulesCreateRequest) (err error)
SaveBatch(req request.MenuModulesBatchCreateRequest) (err error)
Update(id uint, req request.MenuModulesUpdateRequest) (err error)
Delete(id uint) error
}
// NewMenuModulesService init MenuModulesService
func NewMenuModulesService(repo repository.MenuModulesRepository, log zerolog.Logger) MenuModulesService {
return &menuModulesService{
Repo: repo,
Log: log,
}
}
// All implement interface of MenuModulesService
func (_i *menuModulesService) All(req request.MenuModulesQueryRequest) (menuModules []*response.MenuModulesResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
for _, result := range results {
menuModules = append(menuModules, mapper.MenuModulesResponseMapper(result))
}
return
}
func (_i *menuModulesService) GetByMenuId(menuId uint) (menuModules []*response.MenuModulesResponse, err error) {
results, err := _i.Repo.GetByMenuId(menuId)
if err != nil {
return nil, err
}
for _, result := range results {
menuModules = append(menuModules, mapper.MenuModulesResponseMapper(result))
}
return
}
func (_i *menuModulesService) GetByModuleId(moduleId uint) (menuModules []*response.MenuModulesResponse, err error) {
results, err := _i.Repo.GetByModuleId(moduleId)
if err != nil {
return nil, err
}
for _, result := range results {
menuModules = append(menuModules, mapper.MenuModulesResponseMapper(result))
}
return
}
func (_i *menuModulesService) Show(id uint) (menuModule *response.MenuModulesResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
return mapper.MenuModulesResponseMapper(result), nil
}
func (_i *menuModulesService) Save(req request.MenuModulesCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Creating menu module")
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
menuModule := &entity.MenuModules{
MenuId: req.MenuId,
ModuleId: req.ModuleId,
Position: req.Position,
ClientId: req.ClientId,
IsActive: &isActive,
}
return _i.Repo.Create(menuModule)
}
func (_i *menuModulesService) SaveBatch(req request.MenuModulesBatchCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Creating menu modules batch")
isActive := true
var menuModules []*entity.MenuModules
for idx, moduleId := range req.ModuleIds {
position := idx + 1
menuModule := &entity.MenuModules{
MenuId: req.MenuId,
ModuleId: moduleId,
Position: &position,
ClientId: req.ClientId,
IsActive: &isActive,
}
menuModules = append(menuModules, menuModule)
}
return _i.Repo.CreateBatch(menuModules)
}
func (_i *menuModulesService) Update(id uint, req request.MenuModulesUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Updating menu module")
menuModule := &entity.MenuModules{}
if req.MenuId != nil {
menuModule.MenuId = *req.MenuId
}
if req.ModuleId != nil {
menuModule.ModuleId = *req.ModuleId
}
if req.Position != nil {
menuModule.Position = req.Position
}
if req.ClientId != nil {
menuModule.ClientId = req.ClientId
}
if req.IsActive != nil {
menuModule.IsActive = req.IsActive
}
return _i.Repo.Update(id, menuModule)
}
func (_i *menuModulesService) 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,14 @@
package controller
import "netidhub-saas-be/app/module/user_level_module_accesses/service"
type Controller struct {
UserLevelModuleAccesses UserLevelModuleAccessesController
}
func NewController(userLevelModuleAccessesService service.UserLevelModuleAccessesService) *Controller {
return &Controller{
UserLevelModuleAccesses: NewUserLevelModuleAccessesController(userLevelModuleAccessesService),
}
}

View File

@ -0,0 +1,330 @@
package controller
import (
"netidhub-saas-be/app/module/user_level_module_accesses/request"
"netidhub-saas-be/app/module/user_level_module_accesses/response"
"netidhub-saas-be/app/module/user_level_module_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 userLevelModuleAccessesController struct {
userLevelModuleAccessesService service.UserLevelModuleAccessesService
}
type UserLevelModuleAccessesController interface {
All(c *fiber.Ctx) error
GetByUserLevelId(c *fiber.Ctx) error
GetByModuleId(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 NewUserLevelModuleAccessesController(userLevelModuleAccessesService service.UserLevelModuleAccessesService) UserLevelModuleAccessesController {
return &userLevelModuleAccessesController{
userLevelModuleAccessesService: userLevelModuleAccessesService,
}
}
// All UserLevelModuleAccesses
// @Summary Get all UserLevelModuleAccesses
// @Description API for getting all UserLevelModuleAccesses
// @Tags UserLevelModuleAccesses
// @Security Bearer
// @Param user_level_id query int false "User Level ID"
// @Param module_id query int false "Module ID"
// @Param can_access query bool false "Can Access"
// @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-module-accesses [get]
func (_i *userLevelModuleAccessesController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
req := request.UserLevelModuleAccessesQueryRequest{
Pagination: paginate,
}
if userLevelId := c.Query("user_level_id"); userLevelId != "" {
id, _ := strconv.ParseUint(userLevelId, 10, 0)
userLevelIdUint := uint(id)
req.UserLevelId = &userLevelIdUint
}
if moduleId := c.Query("module_id"); moduleId != "" {
id, _ := strconv.ParseUint(moduleId, 10, 0)
moduleIdUint := uint(id)
req.ModuleId = &moduleIdUint
}
if canAccess := c.Query("can_access"); canAccess != "" {
canAccessBool := canAccess == "true"
req.CanAccess = &canAccessBool
}
accessesData, paging, err := _i.userLevelModuleAccessesService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelModuleAccesses list successfully retrieved"},
Data: accessesData,
Meta: paging,
})
}
// GetByUserLevelId get UserLevelModuleAccesses by User Level ID
// @Summary Get UserLevelModuleAccesses by User Level ID
// @Description API for getting UserLevelModuleAccesses by User Level ID
// @Tags UserLevelModuleAccesses
// @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-module-accesses/user-level/{user_level_id} [get]
func (_i *userLevelModuleAccessesController) GetByUserLevelId(c *fiber.Ctx) error {
userLevelId, err := strconv.ParseUint(c.Params("user_level_id"), 10, 0)
if err != nil {
return err
}
accessesData, err := _i.userLevelModuleAccessesService.GetByUserLevelId(uint(userLevelId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelModuleAccesses by user level successfully retrieved"},
Data: accessesData,
})
}
// GetByModuleId get UserLevelModuleAccesses by Module ID
// @Summary Get UserLevelModuleAccesses by Module ID
// @Description API for getting UserLevelModuleAccesses by Module ID
// @Tags UserLevelModuleAccesses
// @Security Bearer
// @Param module_id path int true "Module ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-module-accesses/module/{module_id} [get]
func (_i *userLevelModuleAccessesController) GetByModuleId(c *fiber.Ctx) error {
moduleId, err := strconv.ParseUint(c.Params("module_id"), 10, 0)
if err != nil {
return err
}
accessesData, err := _i.userLevelModuleAccessesService.GetByModuleId(uint(moduleId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelModuleAccesses by module successfully retrieved"},
Data: accessesData,
})
}
// CheckAccess check if user level has access to module
// @Summary Check user level module access
// @Description API for checking if user level has access to module
// @Tags UserLevelModuleAccesses
// @Security Bearer
// @Param payload body request.CheckAccessRequest 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-module-accesses/check-access [post]
func (_i *userLevelModuleAccessesController) CheckAccess(c *fiber.Ctx) error {
req := new(request.CheckAccessRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
hasAccess, err := _i.userLevelModuleAccessesService.CheckAccess(req.UserLevelId, req.ModuleId)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Access check completed"},
Data: response.CheckAccessResponse{HasAccess: hasAccess},
})
}
// Show get one UserLevelModuleAccess
// @Summary Get one UserLevelModuleAccess
// @Description API for getting one UserLevelModuleAccess
// @Tags UserLevelModuleAccesses
// @Security Bearer
// @Param id path int true "UserLevelModuleAccess ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-module-accesses/{id} [get]
func (_i *userLevelModuleAccessesController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
accessData, err := _i.userLevelModuleAccessesService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelModuleAccess successfully retrieved"},
Data: accessData,
})
}
// Save create UserLevelModuleAccess
// @Summary Create UserLevelModuleAccess
// @Description API for create UserLevelModuleAccess
// @Tags UserLevelModuleAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.UserLevelModuleAccessesCreateRequest 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-module-accesses [post]
func (_i *userLevelModuleAccessesController) Save(c *fiber.Ctx) error {
req := new(request.UserLevelModuleAccessesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.userLevelModuleAccessesService.Save(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelModuleAccess successfully created"},
})
}
// SaveBatch create multiple UserLevelModuleAccesses at once
// @Summary Create multiple UserLevelModuleAccesses
// @Description API for creating multiple UserLevelModuleAccesses at once for a user level
// @Tags UserLevelModuleAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.UserLevelModuleAccessesBatchCreateRequest 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-module-accesses/batch [post]
func (_i *userLevelModuleAccessesController) SaveBatch(c *fiber.Ctx) error {
req := new(request.UserLevelModuleAccessesBatchCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.userLevelModuleAccessesService.SaveBatch(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelModuleAccesses successfully created in batch"},
})
}
// Update UserLevelModuleAccess
// @Summary Update UserLevelModuleAccess
// @Description API for update UserLevelModuleAccess
// @Tags UserLevelModuleAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param payload body request.UserLevelModuleAccessesUpdateRequest true "Required payload"
// @Param id path int true "UserLevelModuleAccess ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /user-level-module-accesses/{id} [put]
func (_i *userLevelModuleAccessesController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.UserLevelModuleAccessesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.userLevelModuleAccessesService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelModuleAccess successfully updated"},
})
}
// Delete UserLevelModuleAccess
// @Summary Delete UserLevelModuleAccess
// @Description API for delete UserLevelModuleAccess (soft delete)
// @Tags UserLevelModuleAccesses
// @Security Bearer
// @Param X-Csrf-Token header string false "Insert the X-Csrf-Token"
// @Param id path int true "UserLevelModuleAccess ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /user-level-module-accesses/{id} [delete]
func (_i *userLevelModuleAccessesController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.userLevelModuleAccessesService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"UserLevelModuleAccess successfully deleted"},
})
}

View File

@ -0,0 +1,42 @@
package mapper
import (
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/user_level_module_accesses/response"
)
func UserLevelModuleAccessesResponseMapper(e *entity.UserLevelModuleAccesses) *response.UserLevelModuleAccessesResponse {
resp := &response.UserLevelModuleAccessesResponse{
ID: e.ID,
UserLevelId: e.UserLevelId,
ModuleId: e.ModuleId,
CanAccess: e.CanAccess,
ClientId: e.ClientId,
IsActive: e.IsActive,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
if e.UserLevel != nil {
resp.UserLevel = &response.UserLevelResponse{
ID: e.UserLevel.ID,
Name: e.UserLevel.Name,
AliasName: e.UserLevel.AliasName,
LevelNumber: e.UserLevel.LevelNumber,
Group: e.UserLevel.Group,
}
}
if e.Module != nil {
resp.Module = &response.ModuleDetailResponse{
ID: e.Module.ID,
Name: e.Module.Name,
Description: e.Module.Description,
PathUrl: e.Module.PathUrl,
ActionType: e.Module.ActionType,
}
}
return resp
}

View File

@ -0,0 +1,145 @@
package repository
import (
"fmt"
"netidhub-saas-be/app/database"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/user_level_module_accesses/request"
"netidhub-saas-be/utils/paginator"
)
type userLevelModuleAccessesRepository struct {
DB *database.Database
}
// UserLevelModuleAccessesRepository define interface of IUserLevelModuleAccessesRepository
type UserLevelModuleAccessesRepository interface {
GetAll(req request.UserLevelModuleAccessesQueryRequest) (accesses []*entity.UserLevelModuleAccesses, paging paginator.Pagination, err error)
GetByUserLevelId(userLevelId uint) (accesses []*entity.UserLevelModuleAccesses, err error)
GetByModuleId(moduleId uint) (accesses []*entity.UserLevelModuleAccesses, err error)
CheckAccess(userLevelId uint, moduleId uint) (hasAccess bool, err error)
FindOne(id uint) (access *entity.UserLevelModuleAccesses, err error)
Create(access *entity.UserLevelModuleAccesses) (err error)
CreateBatch(accesses []*entity.UserLevelModuleAccesses) (err error)
Update(id uint, access *entity.UserLevelModuleAccesses) (err error)
Delete(id uint) (err error)
DeleteByUserLevelId(userLevelId uint) (err error)
DeleteByModuleId(moduleId uint) (err error)
}
func NewUserLevelModuleAccessesRepository(db *database.Database) UserLevelModuleAccessesRepository {
return &userLevelModuleAccessesRepository{
DB: db,
}
}
// implement interface of IUserLevelModuleAccessesRepository
func (_i *userLevelModuleAccessesRepository) GetAll(req request.UserLevelModuleAccessesQueryRequest) (accesses []*entity.UserLevelModuleAccesses, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.UserLevelModuleAccesses{})
query = query.Where("is_active = ?", true)
if req.UserLevelId != nil {
query = query.Where("user_level_id = ?", req.UserLevelId)
}
if req.ModuleId != nil {
query = query.Where("module_id = ?", req.ModuleId)
}
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("Module")
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&accesses).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *userLevelModuleAccessesRepository) GetByUserLevelId(userLevelId uint) (accesses []*entity.UserLevelModuleAccesses, err error) {
query := _i.DB.DB.Model(&entity.UserLevelModuleAccesses{})
query = query.Where("user_level_id = ? AND is_active = ?", userLevelId, true)
query = query.Preload("Module")
err = query.Find(&accesses).Error
return
}
func (_i *userLevelModuleAccessesRepository) GetByModuleId(moduleId uint) (accesses []*entity.UserLevelModuleAccesses, err error) {
query := _i.DB.DB.Model(&entity.UserLevelModuleAccesses{})
query = query.Where("module_id = ? AND is_active = ?", moduleId, true)
query = query.Preload("UserLevel")
err = query.Find(&accesses).Error
return
}
func (_i *userLevelModuleAccessesRepository) CheckAccess(userLevelId uint, moduleId uint) (hasAccess bool, err error) {
var access entity.UserLevelModuleAccesses
query := _i.DB.DB.Model(&entity.UserLevelModuleAccesses{})
query = query.Where("user_level_id = ? AND module_id = ? AND is_active = ?", userLevelId, moduleId, true)
err = query.First(&access).Error
if err != nil {
// Jika tidak ada record, berarti tidak ada akses
return false, nil
}
return access.CanAccess, nil
}
func (_i *userLevelModuleAccessesRepository) FindOne(id uint) (access *entity.UserLevelModuleAccesses, err error) {
query := _i.DB.DB.Preload("UserLevel").Preload("Module")
if err := query.First(&access, id).Error; err != nil {
return nil, err
}
return access, nil
}
func (_i *userLevelModuleAccessesRepository) Create(access *entity.UserLevelModuleAccesses) (err error) {
return _i.DB.DB.Create(access).Error
}
func (_i *userLevelModuleAccessesRepository) CreateBatch(accesses []*entity.UserLevelModuleAccesses) (err error) {
return _i.DB.DB.Create(&accesses).Error
}
func (_i *userLevelModuleAccessesRepository) Update(id uint, access *entity.UserLevelModuleAccesses) (err error) {
return _i.DB.DB.Model(&entity.UserLevelModuleAccesses{}).
Where(&entity.UserLevelModuleAccesses{ID: id}).
Updates(access).Error
}
func (_i *userLevelModuleAccessesRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.UserLevelModuleAccesses{}, id).Error
}
func (_i *userLevelModuleAccessesRepository) DeleteByUserLevelId(userLevelId uint) (err error) {
return _i.DB.DB.Where("user_level_id = ?", userLevelId).Delete(&entity.UserLevelModuleAccesses{}).Error
}
func (_i *userLevelModuleAccessesRepository) DeleteByModuleId(moduleId uint) (err error) {
return _i.DB.DB.Where("module_id = ?", moduleId).Delete(&entity.UserLevelModuleAccesses{}).Error
}

View File

@ -0,0 +1,43 @@
package request
import (
"github.com/google/uuid"
"netidhub-saas-be/utils/paginator"
)
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"`
}
type UserLevelModuleAccessesCreateRequest struct {
UserLevelId uint `json:"user_level_id" validate:"required"`
ModuleId uint `json:"module_id" validate:"required"`
CanAccess bool `json:"can_access"`
ClientId *uuid.UUID `json:"client_id"`
IsActive *bool `json:"is_active"`
}
type UserLevelModuleAccessesBatchCreateRequest struct {
UserLevelId uint `json:"user_level_id" validate:"required"`
ModuleIds []uint `json:"module_ids" validate:"required,min=1"`
CanAccess bool `json:"can_access"`
ClientId *uuid.UUID `json:"client_id"`
}
type UserLevelModuleAccessesUpdateRequest struct {
UserLevelId *uint `json:"user_level_id"`
ModuleId *uint `json:"module_id"`
CanAccess *bool `json:"can_access"`
ClientId *uuid.UUID `json:"client_id"`
IsActive *bool `json:"is_active"`
}
type CheckAccessRequest struct {
UserLevelId uint `json:"user_level_id" validate:"required"`
ModuleId uint `json:"module_id" validate:"required"`
}

View File

@ -0,0 +1,40 @@
package response
import (
"github.com/google/uuid"
"time"
)
type UserLevelModuleAccessesResponse struct {
ID uint `json:"id"`
UserLevelId uint `json:"user_level_id"`
ModuleId uint `json:"module_id"`
CanAccess bool `json:"can_access"`
ClientId *uuid.UUID `json:"client_id"`
IsActive *bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
UserLevel *UserLevelResponse `json:"user_level,omitempty"`
Module *ModuleDetailResponse `json:"module,omitempty"`
}
type UserLevelResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
AliasName string `json:"alias_name"`
LevelNumber int `json:"level_number"`
Group *string `json:"group"`
}
type ModuleDetailResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
PathUrl string `json:"path_url"`
ActionType *string `json:"action_type"`
}
type CheckAccessResponse struct {
HasAccess bool `json:"has_access"`
}

View File

@ -0,0 +1,165 @@
package service
import (
"github.com/rs/zerolog"
"netidhub-saas-be/app/database/entity"
"netidhub-saas-be/app/module/user_level_module_accesses/mapper"
"netidhub-saas-be/app/module/user_level_module_accesses/repository"
"netidhub-saas-be/app/module/user_level_module_accesses/request"
"netidhub-saas-be/app/module/user_level_module_accesses/response"
"netidhub-saas-be/utils/paginator"
)
// UserLevelModuleAccessesService
type userLevelModuleAccessesService struct {
Repo repository.UserLevelModuleAccessesRepository
Log zerolog.Logger
}
// UserLevelModuleAccessesService define interface of IUserLevelModuleAccessesService
type UserLevelModuleAccessesService interface {
All(req request.UserLevelModuleAccessesQueryRequest) (accesses []*response.UserLevelModuleAccessesResponse, paging paginator.Pagination, err error)
GetByUserLevelId(userLevelId uint) (accesses []*response.UserLevelModuleAccessesResponse, err error)
GetByModuleId(moduleId uint) (accesses []*response.UserLevelModuleAccessesResponse, err error)
CheckAccess(userLevelId uint, moduleId uint) (hasAccess bool, err error)
Show(id uint) (access *response.UserLevelModuleAccessesResponse, err error)
Save(req request.UserLevelModuleAccessesCreateRequest) (err error)
SaveBatch(req request.UserLevelModuleAccessesBatchCreateRequest) (err error)
Update(id uint, req request.UserLevelModuleAccessesUpdateRequest) (err error)
Delete(id uint) error
}
// NewUserLevelModuleAccessesService init UserLevelModuleAccessesService
func NewUserLevelModuleAccessesService(repo repository.UserLevelModuleAccessesRepository, log zerolog.Logger) UserLevelModuleAccessesService {
return &userLevelModuleAccessesService{
Repo: repo,
Log: log,
}
}
// All implement interface of UserLevelModuleAccessesService
func (_i *userLevelModuleAccessesService) All(req request.UserLevelModuleAccessesQueryRequest) (accesses []*response.UserLevelModuleAccessesResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
for _, result := range results {
accesses = append(accesses, mapper.UserLevelModuleAccessesResponseMapper(result))
}
return
}
func (_i *userLevelModuleAccessesService) GetByUserLevelId(userLevelId uint) (accesses []*response.UserLevelModuleAccessesResponse, err error) {
results, err := _i.Repo.GetByUserLevelId(userLevelId)
if err != nil {
return nil, err
}
for _, result := range results {
accesses = append(accesses, mapper.UserLevelModuleAccessesResponseMapper(result))
}
return
}
func (_i *userLevelModuleAccessesService) GetByModuleId(moduleId uint) (accesses []*response.UserLevelModuleAccessesResponse, err error) {
results, err := _i.Repo.GetByModuleId(moduleId)
if err != nil {
return nil, err
}
for _, result := range results {
accesses = append(accesses, mapper.UserLevelModuleAccessesResponseMapper(result))
}
return
}
func (_i *userLevelModuleAccessesService) CheckAccess(userLevelId uint, moduleId uint) (hasAccess bool, err error) {
return _i.Repo.CheckAccess(userLevelId, moduleId)
}
func (_i *userLevelModuleAccessesService) Show(id uint) (access *response.UserLevelModuleAccessesResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
return mapper.UserLevelModuleAccessesResponseMapper(result), nil
}
func (_i *userLevelModuleAccessesService) Save(req request.UserLevelModuleAccessesCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Creating user level module access")
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
access := &entity.UserLevelModuleAccesses{
UserLevelId: req.UserLevelId,
ModuleId: req.ModuleId,
CanAccess: req.CanAccess,
ClientId: req.ClientId,
IsActive: &isActive,
}
return _i.Repo.Create(access)
}
func (_i *userLevelModuleAccessesService) SaveBatch(req request.UserLevelModuleAccessesBatchCreateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Creating user level module accesses batch")
isActive := true
var accesses []*entity.UserLevelModuleAccesses
for _, moduleId := range req.ModuleIds {
access := &entity.UserLevelModuleAccesses{
UserLevelId: req.UserLevelId,
ModuleId: moduleId,
CanAccess: req.CanAccess,
ClientId: req.ClientId,
IsActive: &isActive,
}
accesses = append(accesses, access)
}
return _i.Repo.CreateBatch(accesses)
}
func (_i *userLevelModuleAccessesService) Update(id uint, req request.UserLevelModuleAccessesUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("Updating user level module access")
access := &entity.UserLevelModuleAccesses{}
if req.UserLevelId != nil {
access.UserLevelId = *req.UserLevelId
}
if req.ModuleId != nil {
access.ModuleId = *req.ModuleId
}
if req.CanAccess != nil {
access.CanAccess = *req.CanAccess
}
if req.ClientId != nil {
access.ClientId = req.ClientId
}
if req.IsActive != nil {
access.IsActive = req.IsActive
}
return _i.Repo.Update(id, access)
}
func (_i *userLevelModuleAccessesService) 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_module_accesses
import (
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
"netidhub-saas-be/app/module/user_level_module_accesses/controller"
"netidhub-saas-be/app/module/user_level_module_accesses/repository"
"netidhub-saas-be/app/module/user_level_module_accesses/service"
)
// struct of UserLevelModuleAccessesRouter
type UserLevelModuleAccessesRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of UserLevelModuleAccesses module
var NewUserLevelModuleAccessesModule = fx.Options(
// register repository of UserLevelModuleAccesses module
fx.Provide(repository.NewUserLevelModuleAccessesRepository),
// register service of UserLevelModuleAccesses module
fx.Provide(service.NewUserLevelModuleAccessesService),
// register controller of UserLevelModuleAccesses module
fx.Provide(controller.NewController),
// register router of UserLevelModuleAccesses module
fx.Provide(NewUserLevelModuleAccessesRouter),
)
// init UserLevelModuleAccessesRouter
func NewUserLevelModuleAccessesRouter(fiber *fiber.App, controller *controller.Controller) *UserLevelModuleAccessesRouter {
return &UserLevelModuleAccessesRouter{
App: fiber,
Controller: controller,
}
}
// register routes of UserLevelModuleAccesses module
func (_i *UserLevelModuleAccessesRouter) RegisterUserLevelModuleAccessesRoutes() {
// define controllers
userLevelModuleAccessesController := _i.Controller.UserLevelModuleAccesses
// define routes
_i.App.Route("/user-level-module-accesses", func(router fiber.Router) {
router.Get("/", userLevelModuleAccessesController.All)
router.Get("/:id", userLevelModuleAccessesController.Show)
router.Get("/user-level/:user_level_id", userLevelModuleAccessesController.GetByUserLevelId)
router.Get("/module/:module_id", userLevelModuleAccessesController.GetByModuleId)
router.Post("/", userLevelModuleAccessesController.Save)
router.Post("/batch", userLevelModuleAccessesController.SaveBatch)
router.Post("/check-access", userLevelModuleAccessesController.CheckAccess)
router.Put("/:id", userLevelModuleAccessesController.Update)
router.Delete("/:id", userLevelModuleAccessesController.Delete)
})
}

View File

@ -0,0 +1,342 @@
# 📦 Implementation Summary - Menu Module Access Control System
## ✅ Yang Sudah Dibuat
### 1. **Database Entities** (3 files)
#### ✓ `app/database/entity/menu_modules.entity.go`
- Relasi many-to-many antara menu dan modul
- Satu menu bisa punya banyak modul (view, create, edit, delete, etc.)
#### ✓ `app/database/entity/user_level_module_accesses.entity.go`
- Mengatur akses user-level ke modul-modul tertentu
- Kontrol akses granular per modul
#### ✓ `app/database/entity/master_modules.entity.go` (Enhanced)
- Ditambahkan field `action_type` untuk membedakan jenis aksi (view, create, edit, delete, approve, export)
### 2. **Repositories** (2 files)
#### ✓ `app/module/menu_modules/repository/menu_modules.repository.go`
Fungsi yang tersedia:
- `GetAll()` - Get all menu-modules dengan pagination
- `GetByMenuId()` - Get modules berdasarkan menu
- `GetByModuleId()` - Get menus berdasarkan module
- `FindOne()` - Get satu menu-module
- `Create()` - Create satu menu-module
- `CreateBatch()` - Create banyak menu-modules sekaligus
- `Update()` - Update menu-module
- `Delete()` - Delete menu-module
- `DeleteByMenuId()` - Delete semua modules dari menu
- `DeleteByModuleId()` - Delete semua menus dari module
#### ✓ `app/module/user_level_module_accesses/repository/user_level_module_accesses.repository.go`
Fungsi yang tersedia:
- `GetAll()` - Get all accesses dengan pagination
- `GetByUserLevelId()` - Get accesses berdasarkan user level
- `GetByModuleId()` - Get accesses berdasarkan module
- `CheckAccess()` - Check apakah user level punya akses ke module
- `FindOne()` - Get satu access
- `Create()` - Create satu access
- `CreateBatch()` - Create banyak accesses sekaligus
- `Update()` - Update access
- `Delete()` - Delete access
### 3. **Services** (2 files)
#### ✓ `app/module/menu_modules/service/menu_modules.service.go`
Business logic untuk mengelola menu-modules
#### ✓ `app/module/user_level_module_accesses/service/user_level_module_accesses.service.go`
Business logic untuk mengelola user level accesses
### 4. **Controllers** (2 files)
#### ✓ `app/module/menu_modules/controller/menu_modules.controller.go`
API Endpoints:
- `GET /menu-modules` - List all
- `GET /menu-modules/:id` - Get one
- `GET /menu-modules/menu/:menu_id` - Get by menu
- `GET /menu-modules/module/:module_id` - Get by module
- `POST /menu-modules` - Create one
- `POST /menu-modules/batch` - Create many
- `PUT /menu-modules/:id` - Update
- `DELETE /menu-modules/:id` - Delete
#### ✓ `app/module/user_level_module_accesses/controller/user_level_module_accesses.controller.go`
API Endpoints:
- `GET /user-level-module-accesses` - List all
- `GET /user-level-module-accesses/:id` - Get one
- `GET /user-level-module-accesses/user-level/:user_level_id` - Get by user level
- `GET /user-level-module-accesses/module/:module_id` - Get by module
- `POST /user-level-module-accesses` - Create one
- `POST /user-level-module-accesses/batch` - Create many
- `POST /user-level-module-accesses/check-access` - Check access
- `PUT /user-level-module-accesses/:id` - Update
- `DELETE /user-level-module-accesses/:id` - Delete
### 5. **Middleware** (1 file)
#### ✓ `app/middleware/module_access.middleware.go`
3 Middleware functions:
1. **`CheckModuleAccess(moduleId|pathUrl)`** - Check akses berdasarkan module ID atau path URL
2. **`CheckModuleAccessByPath()`** - Auto-detect module dari current path
3. **`CheckMenuAccess(menuId)`** - Check akses ke menu (minimal punya 1 akses modul di menu tersebut)
### 6. **Request/Response DTOs** (4 files)
#### ✓ `app/module/menu_modules/request/menu_modules.request.go`
- MenuModulesQueryRequest
- MenuModulesCreateRequest
- MenuModulesBatchCreateRequest
- MenuModulesUpdateRequest
#### ✓ `app/module/menu_modules/response/menu_modules.response.go`
- MenuModulesResponse
- MenuBasicResponse
- ModuleBasicResponse
#### ✓ `app/module/user_level_module_accesses/request/user_level_module_accesses.request.go`
- UserLevelModuleAccessesQueryRequest
- UserLevelModuleAccessesCreateRequest
- UserLevelModuleAccessesBatchCreateRequest
- UserLevelModuleAccessesUpdateRequest
- CheckAccessRequest
#### ✓ `app/module/user_level_module_accesses/response/user_level_module_accesses.response.go`
- UserLevelModuleAccessesResponse
- UserLevelResponse
- ModuleDetailResponse
- CheckAccessResponse
### 7. **Mappers** (2 files)
#### ✓ `app/module/menu_modules/mapper/menu_modules.mapper.go`
- MenuModulesResponseMapper()
#### ✓ `app/module/user_level_module_accesses/mapper/user_level_module_accesses.mapper.go`
- UserLevelModuleAccessesResponseMapper()
### 8. **Module Registration** (2 files)
#### ✓ `app/module/menu_modules/menu_modules.module.go`
- Dependency injection setup
- Route registration
#### ✓ `app/module/user_level_module_accesses/user_level_module_accesses.module.go`
- Dependency injection setup
- Route registration
### 9. **Documentation** (3 files)
#### ✓ `docs/MENU_MODULE_ACCESS_SYSTEM.md`
Dokumentasi lengkap tentang:
- Overview sistem
- Database schema
- API endpoints
- Middleware usage
- Implementation examples
- Best practices
#### ✓ `docs/MENU_MODULE_QUICK_START.md`
Quick start guide dengan langkah-langkah:
- Setup migration
- Setup data master
- Implementasi di code
- Testing
- Troubleshooting
#### ✓ `docs/migrations/004_add_menu_module_access_system.sql`
SQL migration script untuk:
- Alter `master_modules` (add action_type)
- Create `menu_modules` table
- Create `user_level_module_accesses` table
- Indexes dan constraints
- Data migration dari existing structure
- Verification queries
---
## 🚀 Langkah Selanjutnya
### 1. Jalankan Migration
```bash
psql -U your_username -d your_database -f docs/migrations/004_add_menu_module_access_system.sql
```
### 2. Register Modules ke Main App
Edit `app/router/api.go` atau main file untuk register module:
```go
import (
menuModulesModule "netidhub-saas-be/app/module/menu_modules"
userLevelModuleAccessesModule "netidhub-saas-be/app/module/user_level_module_accesses"
)
// Di dalam fx.Options
fx.Provide(
// ... existing modules ...
menuModulesModule.NewMenuModulesModule,
userLevelModuleAccessesModule.NewUserLevelModuleAccessesModule,
),
```
Dan register routes:
```go
func RegisterRoutes(
menuModulesRouter *menuModulesModule.MenuModulesRouter,
userLevelModuleAccessesRouter *userLevelModuleAccessesModule.UserLevelModuleAccessesRouter,
) {
menuModulesRouter.RegisterMenuModulesRoutes()
userLevelModuleAccessesRouter.RegisterUserLevelModuleAccessesRoutes()
}
```
### 3. Setup Data Master
Jalankan SQL untuk membuat modules, menus, dan accesses. Lihat contoh di `docs/MENU_MODULE_QUICK_START.md`.
### 4. Terapkan Middleware ke Routes
Contoh:
```go
func SetupArticleRoutes(app *fiber.App, db *database.Database) {
moduleAccessMw := middleware.NewModuleAccessMiddleware(db)
articles := app.Group("/api/articles")
articles.Get("/",
authMw.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(1)), // View module
articleController.GetAll,
)
articles.Post("/",
authMw.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(2)), // Create module
articleController.Create,
)
}
```
### 5. Testing
Gunakan contoh testing di `docs/MENU_MODULE_QUICK_START.md` untuk validasi implementasi.
---
## 📊 Struktur yang Dibuat
```
netidhub-saas-be/
├── app/
│ ├── database/
│ │ └── entity/
│ │ ├── menu_modules.entity.go ✓
│ │ ├── user_level_module_accesses.entity.go ✓
│ │ └── master_modules.entity.go (Enhanced) ✓
│ ├── middleware/
│ │ └── module_access.middleware.go ✓
│ └── module/
│ ├── menu_modules/
│ │ ├── controller/
│ │ │ ├── controller.go ✓
│ │ │ └── menu_modules.controller.go ✓
│ │ ├── mapper/
│ │ │ └── menu_modules.mapper.go ✓
│ │ ├── repository/
│ │ │ └── menu_modules.repository.go ✓
│ │ ├── request/
│ │ │ └── menu_modules.request.go ✓
│ │ ├── response/
│ │ │ └── menu_modules.response.go ✓
│ │ ├── service/
│ │ │ └── menu_modules.service.go ✓
│ │ └── menu_modules.module.go ✓
│ └── user_level_module_accesses/
│ ├── controller/
│ │ ├── controller.go ✓
│ │ └── user_level_module_accesses.controller.go ✓
│ ├── mapper/
│ │ └── user_level_module_accesses.mapper.go ✓
│ ├── repository/
│ │ └── user_level_module_accesses.repository.go ✓
│ ├── request/
│ │ └── user_level_module_accesses.request.go ✓
│ ├── response/
│ │ └── user_level_module_accesses.response.go ✓
│ ├── service/
│ │ └── user_level_module_accesses.service.go ✓
│ └── user_level_module_accesses.module.go ✓
└── docs/
├── MENU_MODULE_ACCESS_SYSTEM.md ✓
├── MENU_MODULE_QUICK_START.md ✓
└── migrations/
└── 004_add_menu_module_access_system.sql ✓
Total: 27 files created/updated
```
---
## 🎯 Fitur yang Dapat Dilakukan
### ✅ Management Menu-Module
- [x] Menghubungkan banyak modul ke satu menu
- [x] Batch create untuk efisiensi
- [x] Get modules by menu
- [x] Get menus by module
- [x] Manage position/urutan modul
### ✅ Management User Level Access
- [x] Set akses user level ke modul tertentu
- [x] Batch create untuk setup awal
- [x] Check access programmatically
- [x] Get accesses by user level
- [x] Get accesses by module
### ✅ Middleware Protection
- [x] Check akses berdasarkan module ID
- [x] Check akses berdasarkan path URL
- [x] Auto-detect module dari current path
- [x] Check akses menu
- [x] Set context untuk handler
### ✅ API Endpoints
- [x] CRUD menu modules
- [x] CRUD user level accesses
- [x] Check access endpoint
- [x] Batch operations
- [x] Filter by various parameters
---
## 📚 Dokumentasi
Silakan baca dokumentasi lengkap:
1. **[MENU_MODULE_ACCESS_SYSTEM.md](./MENU_MODULE_ACCESS_SYSTEM.md)** - Dokumentasi lengkap
2. **[MENU_MODULE_QUICK_START.md](./MENU_MODULE_QUICK_START.md)** - Quick start guide
---
## 🔧 Support & Troubleshooting
Jika ada masalah, cek:
1. Migration sudah dijalankan dengan benar
2. Tables `menu_modules` dan `user_level_module_accesses` sudah ada
3. Data master sudah disetup
4. Middleware sudah diterapkan dengan benar
5. User sudah terautentikasi sebelum check access
Untuk troubleshooting detail, lihat bagian Troubleshooting di `MENU_MODULE_QUICK_START.md`.
---
**Status: ✅ READY TO USE**
Sistem sudah lengkap dan siap digunakan. Tinggal jalankan migration dan setup data master!

View File

@ -0,0 +1,574 @@
# Menu Module Access Control System
## 📋 Overview
Sistem ini memaksimalkan penggunaan `master_menus` dan `master_modules` untuk mengatur akses berbasis user-level yang lebih granular. Sistem ini memungkinkan:
1. **Menu memiliki banyak modul** - Satu menu dapat terdiri dari berbagai modul (view, create, edit, delete, etc.)
2. **Akses berbasis user-level** - User-level dapat dikonfigurasi untuk mengakses modul-modul tertentu
3. **Pengecekan akses otomatis** - Middleware untuk validasi akses sebelum user mengakses endpoint
## 🗂️ Database Schema
### 1. `master_modules` (Enhanced)
Tabel modul yang sudah ditingkatkan dengan field `action_type`.
```sql
ALTER TABLE master_modules
ADD COLUMN action_type VARCHAR NULL COMMENT 'Tipe aksi: view, create, edit, delete, approve, export, etc';
```
### 2. `menu_modules` (New)
Tabel relasi many-to-many antara menu dan modul.
```sql
CREATE TABLE menu_modules (
id SERIAL PRIMARY KEY,
menu_id INT NOT NULL,
module_id INT NOT 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),
FOREIGN KEY (module_id) REFERENCES master_modules(id),
UNIQUE(menu_id, module_id)
);
CREATE INDEX idx_menu_modules_menu_id ON menu_modules(menu_id);
CREATE INDEX idx_menu_modules_module_id ON menu_modules(module_id);
```
### 3. `user_level_module_accesses` (New)
Tabel untuk mengatur akses user-level ke modul-modul tertentu.
```sql
CREATE TABLE user_level_module_accesses (
id SERIAL PRIMARY KEY,
user_level_id INT NOT NULL,
module_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),
FOREIGN KEY (module_id) REFERENCES master_modules(id),
UNIQUE(user_level_id, module_id)
);
CREATE INDEX idx_user_level_module_accesses_user_level_id ON user_level_module_accesses(user_level_id);
CREATE INDEX idx_user_level_module_accesses_module_id ON user_level_module_accesses(module_id);
```
## 📊 Entity Relationships
```
master_menus (1) ─────< (N) menu_modules (N) >───── (1) master_modules
│ (1)
user_levels (1) ─────< (N) user_level_module_accesses (N) >┘
```
## 🔧 API Endpoints
### Menu Modules API
#### 1. Get All Menu Modules
```http
GET /api/menu-modules?menu_id=1&page=1&limit=10
Authorization: Bearer {token}
```
#### 2. Get Modules by Menu ID
```http
GET /api/menu-modules/menu/{menu_id}
Authorization: Bearer {token}
```
**Response:**
```json
{
"success": true,
"messages": ["MenuModules by menu successfully retrieved"],
"data": [
{
"id": 1,
"menu_id": 1,
"module_id": 5,
"position": 1,
"module": {
"id": 5,
"name": "View Articles",
"description": "View article list",
"path_url": "/api/articles",
"action_type": "view"
}
}
]
}
```
#### 3. Create Menu Module
```http
POST /api/menu-modules
Authorization: Bearer {token}
Content-Type: application/json
{
"menu_id": 1,
"module_id": 5,
"position": 1
}
```
#### 4. Create Menu Modules in Batch
```http
POST /api/menu-modules/batch
Authorization: Bearer {token}
Content-Type: application/json
{
"menu_id": 1,
"module_ids": [5, 6, 7, 8]
}
```
#### 5. Update Menu Module
```http
PUT /api/menu-modules/{id}
Authorization: Bearer {token}
Content-Type: application/json
{
"position": 2,
"is_active": true
}
```
#### 6. Delete Menu Module
```http
DELETE /api/menu-modules/{id}
Authorization: Bearer {token}
```
### User Level Module Accesses API
#### 1. Get All Accesses
```http
GET /api/user-level-module-accesses?user_level_id=1&page=1&limit=10
Authorization: Bearer {token}
```
#### 2. Get Accesses by User Level ID
```http
GET /api/user-level-module-accesses/user-level/{user_level_id}
Authorization: Bearer {token}
```
**Response:**
```json
{
"success": true,
"messages": ["UserLevelModuleAccesses by user level successfully retrieved"],
"data": [
{
"id": 1,
"user_level_id": 1,
"module_id": 5,
"can_access": true,
"module": {
"id": 5,
"name": "View Articles",
"description": "View article list",
"path_url": "/api/articles",
"action_type": "view"
}
}
]
}
```
#### 3. Check Access
```http
POST /api/user-level-module-accesses/check-access
Authorization: Bearer {token}
Content-Type: application/json
{
"user_level_id": 1,
"module_id": 5
}
```
**Response:**
```json
{
"success": true,
"messages": ["Access check completed"],
"data": {
"has_access": true
}
}
```
#### 4. Create Access
```http
POST /api/user-level-module-accesses
Authorization: Bearer {token}
Content-Type: application/json
{
"user_level_id": 1,
"module_id": 5,
"can_access": true
}
```
#### 5. Create Accesses in Batch
```http
POST /api/user-level-module-accesses/batch
Authorization: Bearer {token}
Content-Type: application/json
{
"user_level_id": 1,
"module_ids": [5, 6, 7, 8],
"can_access": true
}
```
## 🛡️ Middleware Usage
### 1. Check Module Access by Module ID
```go
import (
"netidhub-saas-be/app/middleware"
)
// Di router setup
moduleAccessMiddleware := middleware.NewModuleAccessMiddleware(db)
// Protect endpoint dengan module ID
app.Get("/api/articles",
authMiddleware.ValidateToken(),
moduleAccessMiddleware.CheckModuleAccess(uint(5)), // module_id = 5
articleController.GetAll,
)
```
### 2. Check Module Access by Path URL
```go
// Protect endpoint dengan path_url
app.Get("/api/articles",
authMiddleware.ValidateToken(),
moduleAccessMiddleware.CheckModuleAccess("/api/articles"), // path_url
articleController.GetAll,
)
```
### 3. Check Module Access by Current Path (Auto)
```go
// Auto-detect dari path yang sedang diakses
app.Get("/api/articles",
authMiddleware.ValidateToken(),
moduleAccessMiddleware.CheckModuleAccessByPath(), // auto-detect
articleController.GetAll,
)
```
### 4. Check Menu Access
```go
// Check akses ke menu (minimal punya akses ke 1 modul di menu tersebut)
app.Get("/api/articles/menu",
authMiddleware.ValidateToken(),
moduleAccessMiddleware.CheckMenuAccess(uint(1)), // menu_id = 1
articleController.GetByMenu,
)
```
## 📝 Implementation Example
### Scenario: Article Management System
#### Step 1: Create Modules
```sql
-- Insert modules untuk Article
INSERT INTO master_modules (name, description, path_url, action_type, status_id, is_active) VALUES
('View Articles', 'View article list', '/api/articles', 'view', 1, true),
('Create Article', 'Create new article', '/api/articles/create', 'create', 1, true),
('Edit Article', 'Edit existing article', '/api/articles/edit', 'edit', 1, true),
('Delete Article', 'Delete article', '/api/articles/delete', 'delete', 1, true),
('Approve Article', 'Approve article', '/api/articles/approve', 'approve', 1, true);
```
#### Step 2: Create Menu
```sql
-- Insert menu Article
INSERT INTO master_menus (name, description, module_id, icon, "group", position, status_id, is_active) VALUES
('Article Management', 'Manage articles', 1, 'article-icon', 'Content', 1, 1, true);
```
#### Step 3: Link Modules to Menu
```http
POST /api/menu-modules/batch
{
"menu_id": 1,
"module_ids": [1, 2, 3, 4, 5]
}
```
Or via SQL:
```sql
INSERT INTO menu_modules (menu_id, module_id, position, is_active) VALUES
(1, 1, 1, true), -- View Articles
(1, 2, 2, true), -- Create Article
(1, 3, 3, true), -- Edit Article
(1, 4, 4, true), -- Delete Article
(1, 5, 5, true); -- Approve Article
```
#### Step 4: Grant Access to User Levels
**Admin Pusat (user_level_id = 1) - Full Access:**
```http
POST /api/user-level-module-accesses/batch
{
"user_level_id": 1,
"module_ids": [1, 2, 3, 4, 5],
"can_access": true
}
```
**Editor (user_level_id = 2) - Limited Access:**
```http
POST /api/user-level-module-accesses/batch
{
"user_level_id": 2,
"module_ids": [1, 2, 3], // Only view, create, edit
"can_access": true
}
```
**Viewer (user_level_id = 3) - Read Only:**
```http
POST /api/user-level-module-accesses/batch
{
"user_level_id": 3,
"module_ids": [1], // Only view
"can_access": true
}
```
#### Step 5: Protect Routes
```go
func SetupArticleRoutes(app *fiber.App, db *database.Database, articleController controller.ArticleController) {
moduleAccessMw := middleware.NewModuleAccessMiddleware(db)
articles := app.Group("/api/articles")
// View - Accessible by all levels with access
articles.Get("/",
authMiddleware.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(1)), // module_id for "View Articles"
articleController.GetAll,
)
// Create - Accessible by Admin & Editor only
articles.Post("/",
authMiddleware.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(2)), // module_id for "Create Article"
articleController.Create,
)
// Edit - Accessible by Admin & Editor only
articles.Put("/:id",
authMiddleware.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(3)), // module_id for "Edit Article"
articleController.Update,
)
// Delete - Accessible by Admin only
articles.Delete("/:id",
authMiddleware.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(4)), // module_id for "Delete Article"
articleController.Delete,
)
// Approve - Accessible by Admin only
articles.Post("/:id/approve",
authMiddleware.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(5)), // module_id for "Approve Article"
articleController.Approve,
)
}
```
## 🔄 Migration Script
Create file: `docs/migrations/004_add_menu_module_access_system.sql`
```sql
-- Migration: Add Menu Module Access System
-- Description: Enhance master_modules and create menu_modules & user_level_module_accesses tables
-- Step 1: Enhance master_modules with action_type
ALTER TABLE master_modules
ADD COLUMN IF NOT EXISTS action_type VARCHAR NULL;
COMMENT ON COLUMN master_modules.action_type IS 'Tipe aksi: view, create, edit, delete, approve, export, etc';
-- Step 2: Create menu_modules table
CREATE TABLE IF NOT EXISTS menu_modules (
id SERIAL PRIMARY KEY,
menu_id INT NOT NULL,
module_id INT NOT 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,
FOREIGN KEY (module_id) REFERENCES master_modules(id) ON DELETE CASCADE,
UNIQUE(menu_id, module_id, client_id)
);
CREATE INDEX idx_menu_modules_menu_id ON menu_modules(menu_id);
CREATE INDEX idx_menu_modules_module_id ON menu_modules(module_id);
CREATE INDEX idx_menu_modules_client_id ON menu_modules(client_id);
CREATE INDEX idx_menu_modules_is_active ON menu_modules(is_active);
COMMENT ON TABLE menu_modules IS 'Relasi many-to-many antara menu dan modul';
COMMENT ON COLUMN menu_modules.position IS 'Urutan modul dalam menu';
-- Step 3: Create user_level_module_accesses table
CREATE TABLE IF NOT EXISTS user_level_module_accesses (
id SERIAL PRIMARY KEY,
user_level_id INT NOT NULL,
module_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 (module_id) REFERENCES master_modules(id) ON DELETE CASCADE,
UNIQUE(user_level_id, module_id, client_id)
);
CREATE INDEX idx_user_level_module_accesses_user_level_id ON user_level_module_accesses(user_level_id);
CREATE INDEX idx_user_level_module_accesses_module_id ON user_level_module_accesses(module_id);
CREATE INDEX idx_user_level_module_accesses_client_id ON user_level_module_accesses(client_id);
CREATE INDEX idx_user_level_module_accesses_is_active ON user_level_module_accesses(is_active);
COMMENT ON TABLE user_level_module_accesses IS 'Mengatur akses user_level ke modul-modul tertentu';
COMMENT ON COLUMN user_level_module_accesses.can_access IS 'Apakah user level ini boleh akses modul ini';
-- Step 4: Migrate existing data (optional)
-- Copy existing menu.module_id relationship to menu_modules
INSERT INTO menu_modules (menu_id, module_id, position, client_id, is_active, created_at, updated_at)
SELECT
id as menu_id,
module_id,
1 as position,
client_id,
is_active,
created_at,
updated_at
FROM master_menus
WHERE module_id IS NOT NULL
ON CONFLICT (menu_id, module_id, client_id) DO NOTHING;
-- Success message
DO $$
BEGIN
RAISE NOTICE 'Migration completed successfully!';
RAISE NOTICE 'Tables created: menu_modules, user_level_module_accesses';
RAISE NOTICE 'Column added: master_modules.action_type';
END $$;
```
## 🧪 Testing the System
### Test 1: Setup Test Data
```sql
-- Create test modules
INSERT INTO master_modules (name, description, path_url, action_type, status_id) VALUES
('Test View', 'Test view module', '/test/view', 'view', 1),
('Test Create', 'Test create module', '/test/create', 'create', 1),
('Test Edit', 'Test edit module', '/test/edit', 'edit', 1);
-- Create test menu
INSERT INTO master_menus (name, description, module_id, status_id) VALUES
('Test Menu', 'Test menu for module access', 1, 1);
-- Link modules to menu
INSERT INTO menu_modules (menu_id, module_id, position) VALUES
(1, 1, 1),
(1, 2, 2),
(1, 3, 3);
-- Grant access to user level
INSERT INTO user_level_module_accesses (user_level_id, module_id, can_access) VALUES
(1, 1, true), -- Level 1 can view
(1, 2, true), -- Level 1 can create
(2, 1, true); -- Level 2 can only view
```
### Test 2: Check Access via API
```bash
# Check if user level 1 can access module 1 (should return true)
curl -X POST http://localhost:3000/api/user-level-module-accesses/check-access \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"user_level_id": 1,
"module_id": 1
}'
# Check if user level 2 can access module 2 (should return false)
curl -X POST http://localhost:3000/api/user-level-module-accesses/check-access \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{
"user_level_id": 2,
"module_id": 2
}'
```
## 📚 Best Practices
1. **Granular Modules**: Pisahkan setiap action menjadi modul terpisah untuk kontrol akses yang lebih baik
2. **Action Types**: Gunakan action_type konsisten: `view`, `create`, `edit`, `delete`, `approve`, `export`
3. **Batch Operations**: Gunakan batch endpoints untuk mengatur banyak akses sekaligus
4. **Middleware Layers**: Kombinasikan dengan middleware lain (auth, CSRF, rate limit)
5. **Audit Trail**: Log setiap akses yang ditolak untuk security monitoring
6. **Default Deny**: Jika tidak ada record di `user_level_module_accesses`, default adalah tidak ada akses
## 🎯 Benefits
**Kontrol Akses Granular** - Atur akses hingga level action (view, create, edit, delete)
**Fleksibel** - Mudah menambah/mengurangi modul tanpa mengubah kode
**Scalable** - Support multi-client/tenant
**Maintainable** - Struktur yang jelas dan terorganisir
**Secure** - Middleware otomatis block unauthorized access
## 🔗 Related Documentation
- [Multi Client Access Guide](./MULTI_CLIENT_ACCESS_GUIDE.md)
- [Approval Workflow Architecture](../plan/approval-workflow-architecture.md)
- [API Documentation](./notes/api-endpoints-documentation.md)

View File

@ -0,0 +1,317 @@
# Quick Start Guide - Menu Module Access System
## 🚀 Langkah-langkah Setup
### 1. Jalankan Migration
```bash
# Connect ke PostgreSQL dan jalankan migration
psql -U your_username -d your_database -f docs/migrations/004_add_menu_module_access_system.sql
```
### 2. Setup Data Master
#### a. Buat Modules untuk Artikel
```sql
INSERT INTO master_modules (name, description, path_url, action_type, status_id, is_active) VALUES
('Lihat Artikel', 'Melihat daftar dan detail artikel', '/api/articles', 'view', 1, true),
('Buat Artikel', 'Membuat artikel baru', '/api/articles', 'create', 1, true),
('Edit Artikel', 'Mengedit artikel yang ada', '/api/articles/:id', 'edit', 1, true),
('Hapus Artikel', 'Menghapus artikel', '/api/articles/:id', 'delete', 1, true),
('Approve Artikel', 'Menyetujui artikel', '/api/articles/:id/approve', 'approve', 1, true);
-- Dapatkan ID modules yang baru dibuat untuk step selanjutnya
```
#### b. Buat Menu Artikel
```sql
INSERT INTO master_menus (name, description, module_id, icon, "group", position, status_id, is_active)
VALUES ('Artikel', 'Manajemen Artikel', 1, 'article-icon', 'Konten', 1, 1, true);
-- Dapatkan menu_id untuk step selanjutnya (misal: menu_id = 10)
```
#### c. Hubungkan Menu dengan Modules
```sql
-- Asumsikan menu_id = 10, dan module_ids = 1,2,3,4,5
INSERT INTO menu_modules (menu_id, module_id, position, is_active) VALUES
(10, 1, 1, true), -- Lihat
(10, 2, 2, true), -- Buat
(10, 3, 3, true), -- Edit
(10, 4, 4, true), -- Hapus
(10, 5, 5, true); -- Approve
```
#### d. Berikan Akses ke User Levels
```sql
-- Admin Pusat (user_level_id = 1) - Full Access
INSERT INTO user_level_module_accesses (user_level_id, module_id, can_access, is_active) VALUES
(1, 1, true, true), -- Lihat
(1, 2, true, true), -- Buat
(1, 3, true, true), -- Edit
(1, 4, true, true), -- Hapus
(1, 5, true, true); -- Approve
-- Editor (user_level_id = 2) - Lihat, Buat, Edit saja
INSERT INTO user_level_module_accesses (user_level_id, module_id, can_access, is_active) VALUES
(2, 1, true, true), -- Lihat
(2, 2, true, true), -- Buat
(2, 3, true, true); -- Edit
-- Viewer (user_level_id = 3) - Lihat saja
INSERT INTO user_level_module_accesses (user_level_id, module_id, can_access, is_active) VALUES
(3, 1, true, true); -- Lihat
```
### 3. Implementasi di Code
#### a. Tambahkan Routes dengan Middleware
Buat file baru atau update: `app/router/article.routes.go`
```go
package router
import (
"netidhub-saas-be/app/database"
"netidhub-saas-be/app/middleware"
"netidhub-saas-be/app/module/articles/controller"
"github.com/gofiber/fiber/v2"
)
func SetupArticleRoutes(app *fiber.App, db *database.Database, ctrl controller.ArticleController) {
// Initialize middlewares
authMw := middleware.NewUserMiddleware(db)
moduleAccessMw := middleware.NewModuleAccessMiddleware(db)
// Article routes group
articles := app.Group("/api/articles")
// GET /api/articles - View (module_id = 1)
articles.Get("/",
authMw.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(1)),
ctrl.GetAll,
)
// GET /api/articles/:id - View detail (module_id = 1)
articles.Get("/:id",
authMw.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(1)),
ctrl.GetOne,
)
// POST /api/articles - Create (module_id = 2)
articles.Post("/",
authMw.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(2)),
ctrl.Create,
)
// PUT /api/articles/:id - Edit (module_id = 3)
articles.Put("/:id",
authMw.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(3)),
ctrl.Update,
)
// DELETE /api/articles/:id - Delete (module_id = 4)
articles.Delete("/:id",
authMw.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(4)),
ctrl.Delete,
)
// POST /api/articles/:id/approve - Approve (module_id = 5)
articles.Post("/:id/approve",
authMw.ValidateToken(),
moduleAccessMw.CheckModuleAccess(uint(5)),
ctrl.Approve,
)
}
```
#### b. Register Routes di Main Router
Update `app/router/api.go`:
```go
// Import article routes
import (
articleController "netidhub-saas-be/app/module/articles/controller"
)
// Di dalam fungsi RegisterRoutes
func RegisterRoutes(app *fiber.App, db *database.Database) {
// ... existing routes ...
// Article routes with module access control
articleCtrl := articleController.NewArticleController(articleService)
SetupArticleRoutes(app, db, articleCtrl)
}
```
### 4. Testing
#### Test 1: User dengan Full Access (Admin)
```bash
# Login sebagai admin (user_level_id = 1)
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "password"
}'
# Copy token dari response
TOKEN="your_admin_token_here"
# Test akses semua endpoint - Semua harus berhasil
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/articles
curl -X POST -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/articles -d '{"title":"Test"}'
curl -X PUT -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/articles/1 -d '{"title":"Updated"}'
curl -X DELETE -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/articles/1
curl -X POST -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/articles/1/approve
```
#### Test 2: User dengan Limited Access (Editor)
```bash
# Login sebagai editor (user_level_id = 2)
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "editor",
"password": "password"
}'
TOKEN="your_editor_token_here"
# Test akses - View, Create, Edit harus berhasil
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/articles # ✓ Berhasil
curl -X POST -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/articles -d '{"title":"Test"}' # ✓ Berhasil
curl -X PUT -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/articles/1 -d '{"title":"Updated"}' # ✓ Berhasil
# Test akses - Delete dan Approve harus ditolak
curl -X DELETE -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/articles/1 # ✗ 403 Forbidden
curl -X POST -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/articles/1/approve # ✗ 403 Forbidden
```
#### Test 3: Check Access via API
```bash
# Check apakah user level 2 bisa akses module 4 (Delete)
curl -X POST http://localhost:3000/api/user-level-module-accesses/check-access \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user_level_id": 2,
"module_id": 4
}'
# Expected response:
# {
# "success": true,
# "messages": ["Access check completed"],
# "data": {
# "has_access": false
# }
# }
```
### 5. Manage Access via API
#### Berikan Akses Baru
```bash
# Berikan akses Delete ke Editor (user_level_id = 2, module_id = 4)
curl -X POST http://localhost:3000/api/user-level-module-accesses \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user_level_id": 2,
"module_id": 4,
"can_access": true
}'
```
#### Berikan Akses Multiple Modules Sekaligus
```bash
# Berikan akses ke banyak modul sekaligus (batch)
curl -X POST http://localhost:3000/api/user-level-module-accesses/batch \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user_level_id": 4,
"module_ids": [1, 2, 3],
"can_access": true
}'
```
#### Lihat Akses User Level
```bash
# Lihat semua akses untuk user_level_id = 2
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"http://localhost:3000/api/user-level-module-accesses/user-level/2"
```
#### Cabut Akses
```bash
# Update akses menjadi false (cabut akses)
curl -X PUT http://localhost:3000/api/user-level-module-accesses/123 \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"can_access": false
}'
```
## 📋 Checklist Implementation
- [ ] Migration script sudah dijalankan
- [ ] Tabel `menu_modules` dan `user_level_module_accesses` sudah ada
- [ ] Modules sudah dibuat di `master_modules`
- [ ] Menu sudah dibuat di `master_menus`
- [ ] Menu-Module sudah dihubungkan di `menu_modules`
- [ ] User Level Access sudah dikonfigurasi di `user_level_module_accesses`
- [ ] Middleware `ModuleAccessMiddleware` sudah diterapkan di routes
- [ ] Testing berhasil untuk berbagai user level
- [ ] Dokumentasi internal sudah diupdate
## 🎯 Tips & Best Practices
1. **Konsisten dengan Action Type**: Gunakan standar `view`, `create`, `edit`, `delete`, `approve`, `export`
2. **Batch Operations**: Gunakan endpoint batch untuk setup awal atau bulk changes
3. **Soft Delete**: Gunakan `is_active=false` daripada hard delete
4. **Audit Log**: Log setiap perubahan access control untuk audit trail
5. **Default Deny**: Jika tidak ada record = tidak ada akses (secure by default)
## ❓ Troubleshooting
### Error: "User tidak valid"
- Pastikan middleware auth (`ValidateToken()`) dipanggil sebelum `CheckModuleAccess()`
- Pastikan user sudah login dan token valid
### Error: "Module tidak ditemukan"
- Cek module_id yang digunakan di middleware
- Pastikan module exists dan `is_active = true`
### Error: "Anda tidak memiliki akses ke modul ini"
- Cek `user_level_module_accesses` untuk user level tersebut
- Pastikan `can_access = true` dan `is_active = true`
### User Level tidak sesuai
- Cek `user_roles.user_level_id` untuk user tersebut
- Pastikan relasi users -> user_roles -> user_levels sudah benar
## 📚 Selanjutnya
Baca dokumentasi lengkap di: [MENU_MODULE_ACCESS_SYSTEM.md](./MENU_MODULE_ACCESS_SYSTEM.md)

View File

@ -0,0 +1,199 @@
-- Migration: Add Menu Module Access System
-- Description: Enhance master_modules and create menu_modules & user_level_module_accesses tables
-- Date: 2026-01-15
-- ============================================================================
-- Step 1: Enhance master_modules with action_type
-- ============================================================================
ALTER TABLE master_modules
ADD COLUMN IF NOT EXISTS action_type VARCHAR NULL;
COMMENT ON COLUMN master_modules.action_type IS 'Tipe aksi: view, create, edit, delete, approve, export, etc';
-- ============================================================================
-- Step 2: Create menu_modules table
-- ============================================================================
CREATE TABLE IF NOT EXISTS menu_modules (
id SERIAL PRIMARY KEY,
menu_id INT NOT NULL,
module_id INT NOT 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,
FOREIGN KEY (module_id) REFERENCES master_modules(id) ON DELETE CASCADE
);
-- Add unique constraint with client_id consideration
CREATE UNIQUE INDEX idx_menu_modules_unique
ON menu_modules(menu_id, module_id, COALESCE(client_id, '00000000-0000-0000-0000-000000000000'::UUID));
-- Add other indexes for performance
CREATE INDEX idx_menu_modules_menu_id ON menu_modules(menu_id);
CREATE INDEX idx_menu_modules_module_id ON menu_modules(module_id);
CREATE INDEX idx_menu_modules_client_id ON menu_modules(client_id) WHERE client_id IS NOT NULL;
CREATE INDEX idx_menu_modules_is_active ON menu_modules(is_active);
-- Add comments
COMMENT ON TABLE menu_modules IS 'Relasi many-to-many antara menu dan modul. Satu menu bisa punya banyak modul.';
COMMENT ON COLUMN menu_modules.position IS 'Urutan modul dalam menu untuk sorting';
COMMENT ON COLUMN menu_modules.menu_id IS 'Foreign key ke master_menus';
COMMENT ON COLUMN menu_modules.module_id IS 'Foreign key ke master_modules';
-- ============================================================================
-- Step 3: Create user_level_module_accesses table
-- ============================================================================
CREATE TABLE IF NOT EXISTS user_level_module_accesses (
id SERIAL PRIMARY KEY,
user_level_id INT NOT NULL,
module_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 (module_id) REFERENCES master_modules(id) ON DELETE CASCADE
);
-- Add unique constraint with client_id consideration
CREATE UNIQUE INDEX idx_user_level_module_accesses_unique
ON user_level_module_accesses(user_level_id, module_id, COALESCE(client_id, '00000000-0000-0000-0000-000000000000'::UUID));
-- Add other indexes for performance
CREATE INDEX idx_user_level_module_accesses_user_level_id ON user_level_module_accesses(user_level_id);
CREATE INDEX idx_user_level_module_accesses_module_id ON user_level_module_accesses(module_id);
CREATE INDEX idx_user_level_module_accesses_client_id ON user_level_module_accesses(client_id) WHERE client_id IS NOT NULL;
CREATE INDEX idx_user_level_module_accesses_is_active ON user_level_module_accesses(is_active);
CREATE INDEX idx_user_level_module_accesses_can_access ON user_level_module_accesses(can_access);
-- Add comments
COMMENT ON TABLE user_level_module_accesses IS 'Mengatur akses user_level ke modul-modul tertentu. Kontrol akses granular per modul.';
COMMENT ON COLUMN user_level_module_accesses.can_access IS 'Apakah user level ini boleh akses modul ini. True=boleh, False=tidak boleh';
COMMENT ON COLUMN user_level_module_accesses.user_level_id IS 'Foreign key ke user_levels';
COMMENT ON COLUMN user_level_module_accesses.module_id IS 'Foreign key ke master_modules';
-- ============================================================================
-- Step 4: Migrate existing data (optional - only if you have existing data)
-- ============================================================================
-- Copy existing menu.module_id relationship to menu_modules
-- This creates initial menu-module relationships from existing master_menus
INSERT INTO menu_modules (menu_id, module_id, position, client_id, is_active, created_at, updated_at)
SELECT
id as menu_id,
module_id,
position as position, -- Use existing position if available
client_id,
is_active,
created_at,
updated_at
FROM master_menus
WHERE module_id IS NOT NULL
ON CONFLICT DO NOTHING;
-- ============================================================================
-- Step 5: Insert sample modules for testing (optional)
-- ============================================================================
-- You can uncomment this section if you want sample data
/*
-- Sample modules for Article Management
INSERT INTO master_modules (name, description, path_url, action_type, status_id, is_active) VALUES
('View Articles', 'View article list and details', '/api/articles', 'view', 1, true),
('Create Article', 'Create new article', '/api/articles/create', 'create', 1, true),
('Edit Article', 'Edit existing article', '/api/articles/edit', 'edit', 1, true),
('Delete Article', 'Delete article', '/api/articles/delete', 'delete', 1, true),
('Approve Article', 'Approve article submission', '/api/articles/approve', 'approve', 1, true),
('Export Articles', 'Export articles to file', '/api/articles/export', 'export', 1, true)
ON CONFLICT DO NOTHING;
-- Sample modules for User Management
INSERT INTO master_modules (name, description, path_url, action_type, status_id, is_active) VALUES
('View Users', 'View user list and details', '/api/users', 'view', 1, true),
('Create User', 'Create new user', '/api/users/create', 'create', 1, true),
('Edit User', 'Edit existing user', '/api/users/edit', 'edit', 1, true),
('Delete User', 'Delete user', '/api/users/delete', 'delete', 1, true)
ON CONFLICT DO NOTHING;
*/
-- ============================================================================
-- Step 6: Verification queries
-- ============================================================================
-- Run these queries to verify the migration
-- Check if tables exist
DO $$
DECLARE
v_menu_modules_exists BOOLEAN;
v_user_level_module_accesses_exists BOOLEAN;
v_action_type_exists BOOLEAN;
BEGIN
-- Check menu_modules table
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'menu_modules'
) INTO v_menu_modules_exists;
-- Check user_level_module_accesses table
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'user_level_module_accesses'
) INTO v_user_level_module_accesses_exists;
-- Check action_type column
SELECT EXISTS (
SELECT FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'master_modules'
AND column_name = 'action_type'
) INTO v_action_type_exists;
-- Report results
RAISE NOTICE '========================================';
RAISE NOTICE 'Migration Verification Results:';
RAISE NOTICE '========================================';
IF v_menu_modules_exists THEN
RAISE NOTICE '✓ Table menu_modules created successfully';
ELSE
RAISE WARNING '✗ Table menu_modules NOT created';
END IF;
IF v_user_level_module_accesses_exists THEN
RAISE NOTICE '✓ Table user_level_module_accesses created successfully';
ELSE
RAISE WARNING '✗ Table user_level_module_accesses NOT created';
END IF;
IF v_action_type_exists THEN
RAISE NOTICE '✓ Column action_type added to master_modules';
ELSE
RAISE WARNING '✗ Column action_type NOT added to master_modules';
END IF;
RAISE NOTICE '========================================';
IF v_menu_modules_exists AND v_user_level_module_accesses_exists AND v_action_type_exists THEN
RAISE NOTICE 'Migration completed successfully! ✓';
ELSE
RAISE WARNING 'Migration completed with errors! Please check above.';
END IF;
RAISE NOTICE '========================================';
END $$;
-- Show table statistics
SELECT
'menu_modules' as table_name,
COUNT(*) as row_count
FROM menu_modules
UNION ALL
SELECT
'user_level_module_accesses' as table_name,
COUNT(*) as row_count
FROM user_level_module_accesses;

BIN
test-build.exe Normal file

Binary file not shown.