kontenhumas-be/docs/MENU_MODULE_ACCESS_SYSTEM.md

575 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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