# Module Permissions Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Replace hardcoded user permissions with a database-driven module access system managed through the admin UI. **Architecture:** New `users_permissions` table with boolean columns per module. `PermissionRepository` reads/writes permissions. Existing `controls\Users::permissions()` method is refactored to query DB instead of hardcoded array. Admin UI gets inline checkboxes on the user list with AJAX save. **Tech Stack:** PHP 7.4, MySQL (Medoo query builder), jQuery AJAX, Bootstrap 3 --- ### Task 1: Database migration — create `users_permissions` table **Files:** - Modify: `index.php:58-64` (add migration block) **Step 1: Add auto-migration to index.php** In `index.php`, there is already a migration pattern at lines 58-64 (checking for `remember_token` column). Add a second migration block right after it (inside the same `if ( empty( $_SESSION['_db_migrated'] ) )` check) that creates the `users_permissions` table if it doesn't exist: ```php $tbl = $mdb -> query( "SHOW TABLES LIKE 'users_permissions'" ) -> fetch(); if ( !$tbl ) { $mdb -> pdo -> exec( " CREATE TABLE `users_permissions` ( `user_id` INT UNSIGNED NOT NULL PRIMARY KEY, `tasks` TINYINT(1) NOT NULL DEFAULT 1, `projects` TINYINT(1) NOT NULL DEFAULT 1, `finances` TINYINT(1) NOT NULL DEFAULT 0, `wiki` TINYINT(1) NOT NULL DEFAULT 1, `crm` TINYINT(1) NOT NULL DEFAULT 0, `work_time` TINYINT(1) NOT NULL DEFAULT 1, `zaplecze` TINYINT(1) NOT NULL DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8 " ); } ``` Note: No FOREIGN KEY constraint since we don't know if the `users.id` column is INT UNSIGNED. The application handles integrity. **Step 2: Commit** ``` feat: Add users_permissions table auto-migration ``` --- ### Task 2: Create PermissionRepository **Files:** - Create: `autoload/Domain/Users/PermissionRepository.php` **Step 1: Write the test** Create `tests/Domain/Users/PermissionRepositoryTest.php`: ```php 1, 'projects' => 1, 'finances' => 0, 'wiki' => 1, 'crm' => 0, 'work_time' => 1, 'zaplecze' => 0 ]; private $mdb; public function __construct( $mdb = null ) { if ( $mdb ) $this -> mdb = $mdb; else if ( isset( $GLOBALS['mdb'] ) ) $this -> mdb = $GLOBALS['mdb']; else $this -> mdb = null; } public static function defaults() { return self::DEFAULTS; } public function byUserId( $user_id ) { if ( !$this -> mdb ) return self::defaults(); $row = $this -> mdb -> get( 'users_permissions', '*', [ 'user_id' => (int)$user_id ] ); if ( !$row ) return self::defaults(); $result = []; foreach ( self::MODULES as $module ) $result[ $module ] = isset( $row[ $module ] ) ? (int)$row[ $module ] : 0; return $result; } public function save( $user_id, array $modules ) { if ( !$this -> mdb ) return; $data = []; foreach ( self::MODULES as $module ) $data[ $module ] = isset( $modules[ $module ] ) ? (int)(bool)$modules[ $module ] : 0; $existing = $this -> mdb -> get( 'users_permissions', 'user_id', [ 'user_id' => (int)$user_id ] ); if ( $existing ) $this -> mdb -> update( 'users_permissions', $data, [ 'user_id' => (int)$user_id ] ); else $this -> mdb -> insert( 'users_permissions', array_merge( [ 'user_id' => (int)$user_id ], $data ) ); } } ``` **Step 5: Run tests to verify they pass** Run: `php tests/run.php` Expected: All pass **Step 6: Commit** ``` feat: Add PermissionRepository for module access management ``` --- ### Task 3: Refactor controls\Users::permissions() to use DB **Files:** - Modify: `autoload/controls/class.Users.php` (method `permissions`, lines 7-44) **Step 1: Write the test** Add to `tests/Controllers/UsersControllerTest.php` (or create a dedicated test — but since `permissions()` is a static method that relies on global $mdb, we test the PermissionRepository integration logic separately). The existing test file pattern is suitable. No new test file needed — the `PermissionRepository` tests cover the data layer, and the `permissions()` method is a thin wrapper. The integration is tested by checking the app in browser. **Step 2: Replace permissions() method body** Replace the entire `permissions()` method in `autoload/controls/class.Users.php`: ```php public static function permissions( $user_id, $module = '', $action = '' ) { // Superadmin has full access if ( (int)$user_id === 1 ) return true; // Cache permissions per user to avoid repeated DB queries static $cache = []; if ( !isset( $cache[ $user_id ] ) ) { $repo = new \Domain\Users\PermissionRepository(); $cache[ $user_id ] = $repo -> byUserId( (int)$user_id ); } if ( $module && isset( $cache[ $user_id ][ $module ] ) ) return (bool)$cache[ $user_id ][ $module ]; // If module not in permissions list, allow by default return true; } ``` **Step 3: Run tests** Run: `php tests/run.php` Expected: All pass **Step 4: Commit** ``` feat: Refactor permissions() to read from users_permissions table ``` --- ### Task 4: Add AJAX endpoint for saving permissions **Files:** - Modify: `autoload/Controllers/UsersController.php` (add `permissionSave` method) **Step 1: Add permissionSave() method to UsersController** Add after the `vacationLimitSave()` method: ```php public static function permissionSave() { global $user, $mdb; header( 'Content-Type: application/json; charset=utf-8' ); $response = [ 'status' => 'error', 'msg' => 'Wystapil blad podczas zapisywania uprawnien.' ]; if ( !$user || !self::canManageUsers( $user, self::getImpersonatorUser() ) ) { $response['msg'] = 'Brak uprawnien.'; echo json_encode( $response ); exit; } if ( !\S::csrf_verify() ) { $response['msg'] = 'Nieprawidlowy token bezpieczenstwa. Odswiez strone.'; echo json_encode( $response ); exit; } $target_user_id = (int) \S::get( 'user_id' ); $module = \S::get( 'module' ); $value = (int) \S::get( 'value' ); if ( !$target_user_id || !$module ) { echo json_encode( $response ); exit; } // Superadmin permissions cannot be changed if ( $target_user_id === self::ADMIN_USER_ID ) { $response['msg'] = 'Nie mozna zmieniac uprawnien administratora.'; echo json_encode( $response ); exit; } // Whitelist check if ( !in_array( $module, \Domain\Users\PermissionRepository::MODULES, true ) ) { $response['msg'] = 'Nieznany modul.'; echo json_encode( $response ); exit; } $repo = new \Domain\Users\PermissionRepository( $mdb ); $current = $repo -> byUserId( $target_user_id ); $current[ $module ] = $value ? 1 : 0; $repo -> save( $target_user_id, $current ); $response = [ 'status' => 'success', 'msg' => 'Uprawnienia zostaly zapisane.' ]; echo json_encode( $response ); exit; } ``` **Step 2: Run tests** Run: `php tests/run.php` Expected: All pass (no existing tests broken) **Step 3: Commit** ``` feat: Add AJAX endpoint for saving user module permissions ``` --- ### Task 5: Add permissions UI to user list template **Files:** - Modify: `templates/users/main-view.php` - Modify: `autoload/Controllers/UsersController.php` (`buildMainViewModel` and `mainView`) **Step 1: Pass permissions data to the view** In `UsersController::mainView()`, before the `return \Tpl::view(...)` call, load permissions for all users: ```php $permission_repo = new \Domain\Users\PermissionRepository( $mdb ); $permissions_map = []; foreach ( $users_repository -> all() as $u ) { if ( (int)$u['id'] !== self::ADMIN_USER_ID ) $permissions_map[ (int)$u['id'] ] = $permission_repo -> byUserId( (int)$u['id'] ); } ``` Update `buildMainViewModel` to accept and include `$permissions_map` and the modules list: Add parameters to the method signature and add to the returned array: ```php 'permissions_map' => $permissions_map, 'modules' => \Domain\Users\PermissionRepository::MODULES, 'module_labels' => [ 'tasks' => 'Zadania', 'projects' => 'Projekty', 'work_time' => 'Czas pracy', 'finances' => 'Finanse', 'crm' => 'CRM', 'wiki' => 'Wiki', 'zaplecze' => 'Zaplecze' ] ``` **Step 2: Update the template** In `templates/users/main-view.php`: 1. Add "Uprawnienia" column header after "Email" th: ```html Uprawnienia ``` 2. Add permissions cell after the email td, inside the foreach loop: ```php Pelny dostep permissions_map[ (int)$user_tmp['id'] ] ) ):?> modules as $mod ):?> ``` 3. Update colspan in the "Brak uzytkownikow" row from 4 to 5. 4. Add JavaScript at the bottom of the template for AJAX handling: ```html ``` **Step 3: Update buildMainViewModel signature and callers** Update `buildMainViewModel` to accept `$permissions_map` as a 4th parameter: ```php public static function buildMainViewModel( $current_user, $impersonator_user, array $users, array $permissions_map = [] ) { return [ 'current_user' => $current_user, 'impersonator_user' => $impersonator_user, 'users' => $users, 'active_tab' => 'users', 'can_switch_back' => is_array( $impersonator_user ) and isset( $impersonator_user['id'] ) and (int)$impersonator_user['id'] === self::ADMIN_USER_ID, 'permissions_map' => $permissions_map, 'modules' => \Domain\Users\PermissionRepository::MODULES, 'module_labels' => [ 'tasks' => 'Zadania', 'projects' => 'Projekty', 'work_time' => 'Czas pracy', 'finances' => 'Finanse', 'crm' => 'CRM', 'wiki' => 'Wiki', 'zaplecze' => 'Zaplecze' ] ]; } ``` Update the `mainView()` call to pass `$permissions_map`. **Step 4: Fix the test for buildMainViewModel** In `tests/Controllers/UsersControllerTest.php`, the test calls `buildMainViewModel` with 3 args. Update to pass a 4th arg `[]` (empty permissions_map): ```php $view_model = UsersController::buildMainViewModel( $target_user, $admin_user, [ $admin_user, $regular_user, $target_user ], [] ); ``` **Step 5: Run tests** Run: `php tests/run.php` Expected: All pass **Step 6: Commit** ``` feat: Add permissions checkboxes to user management UI ``` --- ### Task 6: Manual testing checklist 1. Log in as admin (ID=1) and go to `/users/main_view/` 2. Verify you see checkboxes for all users except admin 3. Uncheck "Finanse" for a user, verify AJAX returns success 4. Log in as that user (via "Zaloguj jako"), verify "Finanse" is missing from sidebar 5. Try accessing `/finances/main_view/` directly — should get empty/blocked response 6. Switch back to admin, re-check "Finanse", verify it reappears 7. Verify admin always sees all modules regardless of DB state