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