Compare commits

...

11 Commits

81 changed files with 14790 additions and 132 deletions

View File

@ -7,39 +7,40 @@ import (
) )
type Articles struct { type Articles struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"` ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"` Title string `json:"title" gorm:"type:varchar"`
Slug string `json:"slug" gorm:"type:varchar"` Slug string `json:"slug" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"` Description string `json:"description" gorm:"type:varchar"`
CategoryId int `json:"category_id" gorm:"type:int4"` CategoryId int `json:"category_id" gorm:"type:int4"`
HtmlDescription string `json:"html_description" gorm:"type:varchar"` HtmlDescription string `json:"html_description" gorm:"type:varchar"`
TypeId int `json:"type_id" gorm:"type:int4"` TypeId int `json:"type_id" gorm:"type:int4"`
Tags string `json:"tags" gorm:"type:varchar"` Tags string `json:"tags" gorm:"type:varchar"`
ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"` PublishedFor *string `json:"published_for" gorm:"type:varchar(100)"`
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"` ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"`
PageUrl *string `json:"page_url" gorm:"type:varchar"` ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"` PageUrl *string `json:"page_url" gorm:"type:varchar"`
AiArticleId *int `json:"ai_article_id" gorm:"type:int4"` CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
CommentCount *int `json:"comment_count" gorm:"type:int4;default:0"` AiArticleId *int `json:"ai_article_id" gorm:"type:int4"`
ShareCount *int `json:"share_count" gorm:"type:int4;default:0"` CommentCount *int `json:"comment_count" gorm:"type:int4;default:0"`
ViewCount *int `json:"view_count" gorm:"type:int4;default:0"` ShareCount *int `json:"share_count" gorm:"type:int4;default:0"`
StatusId *int `json:"status_id" gorm:"type:int4"` ViewCount *int `json:"view_count" gorm:"type:int4;default:0"`
OldId *uint `json:"old_id" gorm:"type:int4"` StatusId *int `json:"status_id" gorm:"type:int4"`
NeedApprovalFrom *int `json:"need_approval_from" gorm:"type:int4"` OldId *uint `json:"old_id" gorm:"type:int4"`
HasApprovedBy *string `json:"has_approved_by" gorm:"type:varchar"` NeedApprovalFrom *int `json:"need_approval_from" gorm:"type:int4"`
WorkflowId *uint `json:"workflow_id" gorm:"type:int4"` HasApprovedBy *string `json:"has_approved_by" gorm:"type:varchar"`
CurrentApprovalStep *int `json:"current_approval_step" gorm:"type:int4;default:0"` // 0=not submitted, 1+=approval step WorkflowId *uint `json:"workflow_id" gorm:"type:int4"`
CurrentApprovalStep *int `json:"current_approval_step" gorm:"type:int4;default:0"` // 0=not submitted, 1+=approval step
// New fields for no-approval support // New fields for no-approval support
BypassApproval *bool `json:"bypass_approval" gorm:"type:bool;default:false"` // true = skip approval process BypassApproval *bool `json:"bypass_approval" gorm:"type:bool;default:false"` // true = skip approval process
ApprovalExempt *bool `json:"approval_exempt" gorm:"type:bool;default:false"` // true = permanently exempt from approval ApprovalExempt *bool `json:"approval_exempt" gorm:"type:bool;default:false"` // true = permanently exempt from approval
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"` IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`
IsBanner *bool `json:"is_banner" gorm:"type:bool;default:false"` IsBanner *bool `json:"is_banner" gorm:"type:bool;default:false"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"` PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsDraft *bool `json:"is_draft" gorm:"type:bool;default:false"` IsDraft *bool `json:"is_draft" gorm:"type:bool;default:false"`
DraftedAt *time.Time `json:"drafted_at" gorm:"type:timestamp"` DraftedAt *time.Time `json:"drafted_at" gorm:"type:timestamp"`
PublishSchedule *string `json:"publish_schedule" gorm:"type:varchar"` PublishSchedule *string `json:"publish_schedule" gorm:"type:varchar"`
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"` IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"` CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"` UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
} }

View File

@ -10,6 +10,7 @@ type MasterModules struct {
Name string `json:"name" gorm:"type:varchar"` Name string `json:"name" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"` Description string `json:"description" gorm:"type:varchar"`
PathUrl string `json:"path_url" 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"` StatusId int `json:"status_id" gorm:"type:int4"`
ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"` ClientId *uuid.UUID `json:"client_id" gorm:"type:UUID"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"` IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`

View File

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

View File

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

View File

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

View File

@ -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

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

View File

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

View File

@ -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

@ -7,23 +7,30 @@ import (
"strings" "strings"
) )
func ArticleCategoriesResponseMapper(articleCategoriesReq *entity.ArticleCategories, host string) (articleCategoriesRes *res.ArticleCategoriesResponse) { // func ArticleCategoriesResponseMapper(articleCategoriesReq *entity.ArticleCategories, host string) (articleCategoriesRes *res.ArticleCategoriesResponse) {
func ArticleCategoriesResponseMapper(
articleCategoriesReq *entity.ArticleCategories,
createdByFullname *string,
host string,
) (articleCategoriesRes *res.ArticleCategoriesResponse) {
if articleCategoriesReq != nil { if articleCategoriesReq != nil {
articleCategoriesRes = &res.ArticleCategoriesResponse{ articleCategoriesRes = &res.ArticleCategoriesResponse{
ID: articleCategoriesReq.ID, ID: articleCategoriesReq.ID,
Title: articleCategoriesReq.Title, Title: articleCategoriesReq.Title,
Description: articleCategoriesReq.Description, Description: articleCategoriesReq.Description,
Slug: articleCategoriesReq.Slug, Slug: articleCategoriesReq.Slug,
ThumbnailPath: articleCategoriesReq.ThumbnailPath, ThumbnailPath: articleCategoriesReq.ThumbnailPath,
ParentId: articleCategoriesReq.ParentId, ParentId: articleCategoriesReq.ParentId,
OldCategoryId: articleCategoriesReq.OldCategoryId, OldCategoryId: articleCategoriesReq.OldCategoryId,
CreatedById: articleCategoriesReq.CreatedById, CreatedById: articleCategoriesReq.CreatedById,
StatusId: articleCategoriesReq.StatusId, CreatedByFullname: createdByFullname,
IsPublish: articleCategoriesReq.IsPublish, StatusId: articleCategoriesReq.StatusId,
PublishedAt: articleCategoriesReq.PublishedAt, IsPublish: articleCategoriesReq.IsPublish,
IsActive: articleCategoriesReq.IsActive, PublishedAt: articleCategoriesReq.PublishedAt,
CreatedAt: articleCategoriesReq.CreatedAt, IsActive: articleCategoriesReq.IsActive,
UpdatedAt: articleCategoriesReq.UpdatedAt, CreatedAt: articleCategoriesReq.CreatedAt,
UpdatedAt: articleCategoriesReq.UpdatedAt,
} }
if articleCategoriesReq.Tags != nil { if articleCategoriesReq.Tags != nil {

View File

@ -20,10 +20,16 @@ type articleCategoriesRepository struct {
Cfg *config.Config Cfg *config.Config
} }
type ArticleCategoryWithCreator struct {
entity.ArticleCategories
CreatedByFullname *string `gorm:"column:created_by_fullname"`
}
// ArticleCategoriesRepository define interface of IArticleCategoriesRepository // ArticleCategoriesRepository define interface of IArticleCategoriesRepository
type ArticleCategoriesRepository interface { type ArticleCategoriesRepository interface {
GetAll(clientId *uuid.UUID, req request.ArticleCategoriesQueryRequest) (articleCategoriess []*entity.ArticleCategories, paging paginator.Pagination, err error) GetAll(clientId *uuid.UUID, req request.ArticleCategoriesQueryRequest) (articleCategoriess []*entity.ArticleCategories, paging paginator.Pagination, err error)
FindOne(clientId *uuid.UUID, id uint) (articleCategories *entity.ArticleCategories, err error) FindOne(clientId *uuid.UUID, id uint) (articleCategories *entity.ArticleCategories, err error)
FindOneWithCreator(clientId *uuid.UUID, id uint) (*ArticleCategoryWithCreator, error)
FindOneByOldId(clientId *uuid.UUID, id uint) (articleCategories *entity.ArticleCategories, err error) FindOneByOldId(clientId *uuid.UUID, id uint) (articleCategories *entity.ArticleCategories, err error)
FindOneBySlug(clientId *uuid.UUID, slug string) (articleCategories *entity.ArticleCategories, err error) FindOneBySlug(clientId *uuid.UUID, slug string) (articleCategories *entity.ArticleCategories, err error)
Create(articleCategories *entity.ArticleCategories) (articleCategoriesReturn *entity.ArticleCategories, err error) Create(articleCategories *entity.ArticleCategories) (articleCategoriesReturn *entity.ArticleCategories, err error)
@ -174,3 +180,29 @@ func (_i *articleCategoriesRepository) Delete(clientId *uuid.UUID, id uint) erro
} }
return query.Delete(&entity.ArticleCategories{}).Error return query.Delete(&entity.ArticleCategories{}).Error
} }
func (_i *articleCategoriesRepository) FindOneWithCreator(
clientId *uuid.UUID,
id uint,
) (*ArticleCategoryWithCreator, error) {
var result ArticleCategoryWithCreator
query := _i.DB.DB.Table("article_categories ac").
Select(`
ac.*,
u.fullname AS created_by_fullname
`).
Joins("LEFT JOIN users u ON u.id = ac.created_by_id").
Where("ac.id = ?", id)
if clientId != nil {
query = query.Where("ac.client_id = ?", clientId)
}
if err := query.Scan(&result).Error; err != nil {
return nil, err
}
return &result, nil
}

View File

@ -56,6 +56,7 @@ type ArticleCategoriesUpdateRequest struct {
CreatedById *uint `json:"createdById"` CreatedById *uint `json:"createdById"`
IsPublish *bool `json:"isPublish"` IsPublish *bool `json:"isPublish"`
PublishedAt *time.Time `json:"publishedAt"` PublishedAt *time.Time `json:"publishedAt"`
IsActive *bool `json:"isActive"`
} }
func (req ArticleCategoriesUpdateRequest) ToEntity() *entity.ArticleCategories { func (req ArticleCategoriesUpdateRequest) ToEntity() *entity.ArticleCategories {
@ -67,6 +68,7 @@ func (req ArticleCategoriesUpdateRequest) ToEntity() *entity.ArticleCategories {
Slug: req.Slug, Slug: req.Slug,
Tags: req.Tags, Tags: req.Tags,
StatusId: req.StatusId, StatusId: req.StatusId,
IsActive: req.IsActive,
IsPublish: req.IsPublish, IsPublish: req.IsPublish,
PublishedAt: req.PublishedAt, PublishedAt: req.PublishedAt,
UpdatedAt: time.Now(), UpdatedAt: time.Now(),

View File

@ -3,21 +3,22 @@ package response
import "time" import "time"
type ArticleCategoriesResponse struct { type ArticleCategoriesResponse struct {
ID uint `json:"id"` ID uint `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Description string `json:"description"` Description string `json:"description"`
ThumbnailUrl string `json:"thumbnailUrl"` ThumbnailUrl string `json:"thumbnailUrl"`
Slug *string `json:"slug"` Slug *string `json:"slug"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
ThumbnailPath *string `json:"thumbnailPath"` ThumbnailPath *string `json:"thumbnailPath"`
ParentId *int `json:"parentId"` ParentId *int `json:"parentId"`
OldCategoryId *uint `json:"oldCategoryId"` OldCategoryId *uint `json:"oldCategoryId"`
CreatedById *uint `json:"createdById"` CreatedById *uint `json:"createdById"`
StatusId int `json:"statusId"` CreatedByFullname *string `json:"createdByFullname"`
IsPublish *bool `json:"isPublish"` StatusId int `json:"statusId"`
PublishedAt *time.Time `json:"publishedAt"` IsPublish *bool `json:"isPublish"`
IsEnabled *bool `json:"isEnabled"` PublishedAt *time.Time `json:"publishedAt"`
IsActive *bool `json:"isActive"` IsEnabled *bool `json:"isEnabled"`
CreatedAt time.Time `json:"createdAt"` IsActive *bool `json:"isActive"`
UpdatedAt time.Time `json:"updatedAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} }

View File

@ -2,10 +2,6 @@ package service
import ( import (
"context" "context"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
"io" "io"
"log" "log"
"math/rand" "math/rand"
@ -24,6 +20,11 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
) )
// ArticleCategoriesService // ArticleCategoriesService
@ -86,9 +87,16 @@ func (_i *articleCategoriesService) All(authToken string, req request.ArticleCat
host := _i.Cfg.App.Domain host := _i.Cfg.App.Domain
for _, result := range results { for _, result := range results {
articleCategoriess = append(articleCategoriess, mapper.ArticleCategoriesResponseMapper(result, host)) articleCategoriess = append(
articleCategoriess,
mapper.ArticleCategoriesResponseMapper(result, nil, host),
)
} }
// for _, result := range results {
// articleCategoriess = append(articleCategoriess, mapper.ArticleCategoriesResponseMapper(result, host))
// }
return return
} }
@ -103,12 +111,25 @@ func (_i *articleCategoriesService) Show(authToken string, id uint) (articleCate
} }
} }
result, err := _i.Repo.FindOne(clientId, id) result, err := _i.Repo.FindOneWithCreator(clientId, id)
if err != nil { if err != nil {
return nil, err return nil, err
} }
host := _i.Cfg.App.Domain host := _i.Cfg.App.Domain
return mapper.ArticleCategoriesResponseMapper(result, host), nil
return mapper.ArticleCategoriesResponseMapper(
&result.ArticleCategories,
result.CreatedByFullname,
host,
), nil
// result, err := _i.Repo.FindOne(clientId, id)
// if err != nil {
// return nil, err
// }
// host := _i.Cfg.App.Domain
// return mapper.ArticleCategoriesResponseMapper(result, host), nil
} }
func (_i *articleCategoriesService) ShowByOldId(authToken string, id uint) (articleCategories *response.ArticleCategoriesResponse, err error) { func (_i *articleCategoriesService) ShowByOldId(authToken string, id uint) (articleCategories *response.ArticleCategoriesResponse, err error) {
@ -127,7 +148,8 @@ func (_i *articleCategoriesService) ShowByOldId(authToken string, id uint) (arti
return nil, err return nil, err
} }
host := _i.Cfg.App.Domain host := _i.Cfg.App.Domain
return mapper.ArticleCategoriesResponseMapper(result, host), nil return mapper.ArticleCategoriesResponseMapper(result, nil, host), nil
// return mapper.ArticleCategoriesResponseMapper(result, host), nil
} }
func (_i *articleCategoriesService) ShowBySlug(authToken string, slug string) (articleCategories *response.ArticleCategoriesResponse, err error) { func (_i *articleCategoriesService) ShowBySlug(authToken string, slug string) (articleCategories *response.ArticleCategoriesResponse, err error) {
@ -146,7 +168,8 @@ func (_i *articleCategoriesService) ShowBySlug(authToken string, slug string) (a
return nil, err return nil, err
} }
host := _i.Cfg.App.Domain host := _i.Cfg.App.Domain
return mapper.ArticleCategoriesResponseMapper(result, host), nil return mapper.ArticleCategoriesResponseMapper(result, nil, host), nil
// return mapper.ArticleCategoriesResponseMapper(result, host), nil
} }
func (_i *articleCategoriesService) Save(authToken string, req request.ArticleCategoriesCreateRequest) (articleCategories *entity.ArticleCategories, err error) { func (_i *articleCategoriesService) Save(authToken string, req request.ArticleCategoriesCreateRequest) (articleCategories *entity.ArticleCategories, err error) {

View File

@ -55,7 +55,7 @@ func ArticlesResponseMapper(
if len(articleCategories) > 0 { if len(articleCategories) > 0 {
for _, result := range articleCategories { for _, result := range articleCategories {
if result.Category != nil { if result.Category != nil {
articleCategoriesArr = append(articleCategoriesArr, articleCategoriesMapper.ArticleCategoriesResponseMapper(result.Category, host)) articleCategoriesArr = append(articleCategoriesArr, articleCategoriesMapper.ArticleCategoriesResponseMapper(result.Category, nil, host))
} }
} }
log.Info().Interface("articleCategoriesArr", articleCategoriesArr).Msg("") log.Info().Interface("articleCategoriesArr", articleCategoriesArr).Msg("")
@ -82,6 +82,7 @@ func ArticlesResponseMapper(
HtmlDescription: articlesReq.HtmlDescription, HtmlDescription: articlesReq.HtmlDescription,
TypeId: articlesReq.TypeId, TypeId: articlesReq.TypeId,
Tags: articlesReq.Tags, Tags: articlesReq.Tags,
PublishedFor: articlesReq.PublishedFor,
CategoryId: articlesReq.CategoryId, CategoryId: articlesReq.CategoryId,
AiArticleId: articlesReq.AiArticleId, AiArticleId: articlesReq.AiArticleId,
CategoryName: categoryName, CategoryName: categoryName,

View File

@ -42,6 +42,7 @@ type ArticlesCreateRequest struct {
IsPublish *bool `json:"isPublish"` IsPublish *bool `json:"isPublish"`
IsDraft *bool `json:"isDraft"` IsDraft *bool `json:"isDraft"`
OldId *uint `json:"oldId"` OldId *uint `json:"oldId"`
PublishedFor *string `json:"publishedFor"`
} }
func (req ArticlesCreateRequest) ToEntity() *entity.Articles { func (req ArticlesCreateRequest) ToEntity() *entity.Articles {
@ -56,6 +57,7 @@ func (req ArticlesCreateRequest) ToEntity() *entity.Articles {
IsPublish: req.IsPublish, IsPublish: req.IsPublish,
IsDraft: req.IsDraft, IsDraft: req.IsDraft,
OldId: req.OldId, OldId: req.OldId,
PublishedFor: req.PublishedFor,
} }
} }
@ -73,6 +75,7 @@ type ArticlesUpdateRequest struct {
IsPublish *bool `json:"isPublish"` IsPublish *bool `json:"isPublish"`
IsDraft *bool `json:"isDraft"` IsDraft *bool `json:"isDraft"`
StatusId *int `json:"statusId"` StatusId *int `json:"statusId"`
PublishedFor *string `json:"publishedFor"`
} }
func (req ArticlesUpdateRequest) ToEntity() *entity.Articles { func (req ArticlesUpdateRequest) ToEntity() *entity.Articles {
@ -89,6 +92,7 @@ func (req ArticlesUpdateRequest) ToEntity() *entity.Articles {
IsPublish: req.IsPublish, IsPublish: req.IsPublish,
IsDraft: req.IsDraft, IsDraft: req.IsDraft,
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
PublishedFor: req.PublishedFor,
} }
} else { } else {
return &entity.Articles{ return &entity.Articles{

View File

@ -16,6 +16,7 @@ type ArticlesResponse struct {
CategoryName string `json:"categoryName"` CategoryName string `json:"categoryName"`
TypeId int `json:"typeId"` TypeId int `json:"typeId"`
Tags string `json:"tags"` Tags string `json:"tags"`
PublishedFor *string `json:"publishedFor"`
ThumbnailUrl string `json:"thumbnailUrl"` ThumbnailUrl string `json:"thumbnailUrl"`
PageUrl *string `json:"pageUrl"` PageUrl *string `json:"pageUrl"`
CreatedById *uint `json:"createdById"` CreatedById *uint `json:"createdById"`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,145 @@
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)
// Apply sorting
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
} else {
query.Order("position ASC")
}
// Apply pagination (manual calculation like articles)
page := req.Pagination.Page
limit := req.Pagination.Limit
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 10
}
offset := (page - 1) * limit
err = query.Offset(offset).Limit(limit).Find(&menuModules).Error
if err != nil {
return
}
// Create pagination response
paging = paginator.Pagination{
Page: page,
Limit: limit,
Count: count,
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
}
return
}
func (_i *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 (
"netidhub-saas-be/utils/paginator"
"github.com/google/uuid"
)
type MenuModulesQueryRequest struct {
MenuId *uint `query:"menu_id"`
ModuleId *uint `query:"module_id"`
ClientId *uuid.UUID `query:"client_id"`
Pagination *paginator.Pagination `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,16 @@
package controller
import (
"netidhub-saas-be/app/module/user_level_menu_accesses/service"
)
type Controller struct {
UserLevelMenuAccesses UserLevelMenuAccessesController
}
func NewController(userLevelMenuAccessesService service.UserLevelMenuAccessesService) *Controller {
return &Controller{
UserLevelMenuAccesses: NewUserLevelMenuAccessesController(userLevelMenuAccessesService),
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,160 @@
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)
// Apply sorting
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
// Apply pagination (manual calculation like articles)
page := req.Pagination.Page
limit := req.Pagination.Limit
if page <= 0 {
page = 1
}
if limit <= 0 {
limit = 10
}
offset := (page - 1) * limit
err = query.Offset(offset).Limit(limit).Find(&accesses).Error
if err != nil {
return
}
// Create pagination response
paging = paginator.Pagination{
Page: page,
Limit: limit,
Count: count,
TotalPage: int((count + int64(limit) - 1) / int64(limit)),
}
return
}
func (_i *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.Pagination `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

@ -73,8 +73,8 @@ type UsersUpdateRequest struct {
Username string `json:"username" validate:"required,lowercase"` Username string `json:"username" validate:"required,lowercase"`
Email string `json:"email" validate:"required,email"` Email string `json:"email" validate:"required,email"`
Fullname string `json:"fullname" validate:"required"` Fullname string `json:"fullname" validate:"required"`
UserLevelId uint `json:"userLevelId" validate:"required"` UserLevelId *uint `json:"userLevelId"`
UserRoleId uint `json:"userRoleId" validate:"required"` UserRoleId *uint `json:"userRoleId"`
PhoneNumber *string `json:"phoneNumber"` PhoneNumber *string `json:"phoneNumber"`
Address *string `json:"address"` Address *string `json:"address"`
WorkType *string `json:"workType"` WorkType *string `json:"workType"`
@ -88,8 +88,27 @@ type UsersUpdateRequest struct {
StatusId *int `json:"statusId"` StatusId *int `json:"statusId"`
} }
// type UsersUpdateRequest struct {
// Username string `json:"username" validate:"required,lowercase"`
// Email string `json:"email" validate:"required,email"`
// Fullname string `json:"fullname" validate:"required"`
// UserLevelId uint `json:"userLevelId" validate:"required"`
// UserRoleId uint `json:"userRoleId" validate:"required"`
// PhoneNumber *string `json:"phoneNumber"`
// Address *string `json:"address"`
// WorkType *string `json:"workType"`
// GenderType *string `json:"genderType"`
// IdentityType *string `json:"identityType"`
// IdentityGroup *string `json:"identityGroup"`
// IdentityGroupNumber *string `json:"identityGroupNumber"`
// IdentityNumber *string `json:"identityNumber"`
// DateOfBirth *string `json:"dateOfBirth"`
// LastEducation *string `json:"lastEducation"`
// StatusId *int `json:"statusId"`
// }
func (req UsersUpdateRequest) ToEntity() *users.Users { func (req UsersUpdateRequest) ToEntity() *users.Users {
return &users.Users{ entity := &users.Users{
Username: req.Username, Username: req.Username,
Email: req.Email, Email: req.Email,
Fullname: req.Fullname, Fullname: req.Fullname,
@ -103,13 +122,44 @@ func (req UsersUpdateRequest) ToEntity() *users.Users {
IdentityNumber: req.IdentityNumber, IdentityNumber: req.IdentityNumber,
DateOfBirth: req.DateOfBirth, DateOfBirth: req.DateOfBirth,
LastEducation: req.LastEducation, LastEducation: req.LastEducation,
UserRoleId: req.UserRoleId,
StatusId: req.StatusId, StatusId: req.StatusId,
UserLevelId: req.UserLevelId,
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
// ⬇️ HANYA SET JIKA DIKIRIM
if req.UserLevelId != nil {
entity.UserLevelId = *req.UserLevelId
}
if req.UserRoleId != nil {
entity.UserRoleId = *req.UserRoleId
}
return entity
} }
// func (req UsersUpdateRequest) ToEntity() *users.Users {
// return &users.Users{
// Username: req.Username,
// Email: req.Email,
// Fullname: req.Fullname,
// Address: req.Address,
// PhoneNumber: req.PhoneNumber,
// WorkType: req.WorkType,
// GenderType: req.GenderType,
// IdentityType: req.IdentityType,
// IdentityGroup: req.IdentityGroup,
// IdentityGroupNumber: req.IdentityGroupNumber,
// IdentityNumber: req.IdentityNumber,
// DateOfBirth: req.DateOfBirth,
// LastEducation: req.LastEducation,
// UserRoleId: req.UserRoleId,
// StatusId: req.StatusId,
// UserLevelId: req.UserLevelId,
// UpdatedAt: time.Now(),
// }
// }
type UserLogin struct { type UserLogin struct {
Username *string `json:"username"` Username *string `json:"username"`
Password *string `json:"password"` Password *string `json:"password"`

View File

@ -375,25 +375,80 @@ func (_i *usersService) Update(authToken string, id uint, req request.UsersUpdat
} }
} }
_i.Log.Info().Interface("data", req).Msg("") // 1⃣ Ambil data user lama
newReq := req.ToEntity() existingUser, err := _i.Repo.FindOne(clientId, id)
findUser, err := _i.Repo.FindOne(clientId, id)
if err != nil { if err != nil {
return err return err
} }
err = _i.Keycloak.UpdateUser(findUser.KeycloakId, req.Fullname, req.Email) // 2⃣ Update Keycloak (nama & email)
err = _i.Keycloak.UpdateUser(existingUser.KeycloakId, req.Fullname, req.Email)
if err != nil { if err != nil {
return err return err
} }
// Set ClientId on entity // 3⃣ Update field yang BOLEH diubah
newReq.ClientId = clientId existingUser.Username = req.Username
existingUser.Email = req.Email
existingUser.Fullname = req.Fullname
existingUser.Address = req.Address
existingUser.PhoneNumber = req.PhoneNumber
existingUser.WorkType = req.WorkType
existingUser.GenderType = req.GenderType
existingUser.IdentityType = req.IdentityType
existingUser.IdentityGroup = req.IdentityGroup
existingUser.IdentityGroupNumber = req.IdentityGroupNumber
existingUser.IdentityNumber = req.IdentityNumber
existingUser.DateOfBirth = req.DateOfBirth
existingUser.LastEducation = req.LastEducation
existingUser.StatusId = req.StatusId
existingUser.UpdatedAt = time.Now()
return _i.Repo.Update(clientId, id, newReq) // 4⃣ Role & Level HANYA jika dikirim
if req.UserLevelId != nil {
existingUser.UserLevelId = *req.UserLevelId
}
if req.UserRoleId != nil {
existingUser.UserRoleId = *req.UserRoleId
}
// 5⃣ Pastikan clientId tidak hilang
existingUser.ClientId = clientId
// 6⃣ Simpan
return _i.Repo.Update(clientId, id, existingUser)
} }
// func (_i *usersService) Update(authToken string, id uint, req request.UsersUpdateRequest) (err error) {
// // Extract clientId from authToken
// var clientId *uuid.UUID
// if authToken != "" {
// user := utilSvc.GetUserInfo(_i.Log, _i.Repo, authToken)
// if user != nil && user.ClientId != nil {
// clientId = user.ClientId
// _i.Log.Info().Interface("clientId", clientId).Msg("Extracted clientId from auth token")
// }
// }
// _i.Log.Info().Interface("data", req).Msg("")
// newReq := req.ToEntity()
// findUser, err := _i.Repo.FindOne(clientId, id)
// if err != nil {
// return err
// }
// err = _i.Keycloak.UpdateUser(findUser.KeycloakId, req.Fullname, req.Email)
// if err != nil {
// return err
// }
// // Set ClientId on entity
// newReq.ClientId = clientId
// return _i.Repo.Update(clientId, id, newReq)
// }
func (_i *usersService) Delete(authToken string, id uint) error { func (_i *usersService) Delete(authToken string, id uint) error {
// Extract clientId from authToken // Extract clientId from authToken
var clientId *uuid.UUID var clientId *uuid.UUID

View File

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

View File

@ -7,16 +7,16 @@ domain = "https://kontenhumas.com/api"
external-port = ":8809" external-port = ":8809"
idle-timeout = 5 # As seconds idle-timeout = 5 # As seconds
print-routes = false print-routes = false
prefork = false prefork = true
production = false production = false
body-limit = 1048576000 # "100 * 1024 * 1024" body-limit = 1048576000 # "100 * 1024 * 1024"
primary-client-key = "78356d32-52fa-4dfc-b836-6cebf4e3eead" primary-client-key = "78356d32-52fa-4dfc-b836-6cebf4e3eead"
[db.postgres] [db.postgres]
dsn = "postgresql://netidhub_user:NetidhubDB%402025@38.47.185.79:5432/netidhub_db" # <driver>://<username>:<password>@<host>:<port>/<database> dsn = "postgresql://netidhub_user:NetidhubDB%402025@38.47.185.79:5432/netidhub_db" # <driver>://<username>:<password>@<host>:<port>/<database>
log-mode = "ERROR" log-mode = "NONE"
migrate = true migrate = true
seed = false seed = true
[logger] [logger]
log-dir = "debug.log" log-dir = "debug.log"
@ -25,7 +25,7 @@ level = 0 # panic -> 5, fatal -> 4, error -> 3, warn -> 2, info -> 1, debug -> 0
prettier = true prettier = true
[objectstorage.miniostorage] [objectstorage.miniostorage]
endpoint = "https://is3.cloudhost.id" endpoint = "is3.cloudhost.id"
access-key-id = "YRP1RM617986USRU6NN8" access-key-id = "YRP1RM617986USRU6NN8"
secret-access-key = "vfbwQDYb1m7nfzo4LVEz90BIyOWfBMZ6bfGQbqDO" secret-access-key = "vfbwQDYb1m7nfzo4LVEz90BIyOWfBMZ6bfGQbqDO"
use-ssl = true use-ssl = true

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

View File

@ -0,0 +1,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;

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

BIN
test-build.exe Normal file

Binary file not shown.