kontenhumas-be/docs/MENU_MODULE_ACCESS_SYSTEM.md

16 KiB
Raw Blame History

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.

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.

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.

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

GET /api/menu-modules?menu_id=1&page=1&limit=10
Authorization: Bearer {token}

2. Get Modules by Menu ID

GET /api/menu-modules/menu/{menu_id}
Authorization: Bearer {token}

Response:

{
  "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

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

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

PUT /api/menu-modules/{id}
Authorization: Bearer {token}
Content-Type: application/json

{
  "position": 2,
  "is_active": true
}

6. Delete Menu Module

DELETE /api/menu-modules/{id}
Authorization: Bearer {token}

User Level Module Accesses API

1. Get All Accesses

GET /api/user-level-module-accesses?user_level_id=1&page=1&limit=10
Authorization: Bearer {token}

2. Get Accesses by User Level ID

GET /api/user-level-module-accesses/user-level/{user_level_id}
Authorization: Bearer {token}

Response:

{
  "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

POST /api/user-level-module-accesses/check-access
Authorization: Bearer {token}
Content-Type: application/json

{
  "user_level_id": 1,
  "module_id": 5
}

Response:

{
  "success": true,
  "messages": ["Access check completed"],
  "data": {
    "has_access": true
  }
}

4. Create Access

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

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

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

// 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)

// Auto-detect dari path yang sedang diakses
app.Get("/api/articles", 
    authMiddleware.ValidateToken(),
    moduleAccessMiddleware.CheckModuleAccessByPath(), // auto-detect
    articleController.GetAll,
)

4. Check Menu Access

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

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

-- 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);
POST /api/menu-modules/batch
{
  "menu_id": 1,
  "module_ids": [1, 2, 3, 4, 5]
}

Or via 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:

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:

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:

POST /api/user-level-module-accesses/batch
{
  "user_level_id": 3,
  "module_ids": [1],  // Only view
  "can_access": true
}

Step 5: Protect Routes

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

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

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

# 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