kontenhumas-be/docs/MENU_MODULE_ACCESS_SYSTEM.md

575 lines
16 KiB
Markdown
Raw Normal View History

2026-01-15 09:04:49 +00:00
# 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)