575 lines
16 KiB
Markdown
575 lines
16 KiB
Markdown
|
|
# 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)
|
|||
|
|
|