session->userdata('logged_in')) { $this->rejectUnauthenticatedRequest(); } if ($this->db->table_exists('companies')) { $this->load->library('CompanyContext'); try { $this->companycontext->id(); } catch (BusinessException $exception) { $this->session->unset_userdata(array( 'user_id', 'nama', 'username', 'role', 'role_id', 'is_super_admin', 'permissions', 'company_id', 'logged_in' )); $this->session->set_flashdata('error', $exception->getMessage()); redirect('auth'); exit; } } $this->enforceRoutePermission(); } /** Permission endpoint terpusat; menu tersembunyi bukan satu-satunya kontrol akses. */ protected function enforceRoutePermission() { if ($this->session->userdata('role') === 'Admin' || is_master_admin_user()) return; $class = strtolower((string)$this->router->fetch_class()); $method = strtolower((string)$this->router->fetch_method()); // Draft PR boleh dihapus oleh pembuatnya tanpa can_delete. Otorisasi pemilik // dan status tetap wajib diperiksa dengan row lock di PurchaseService. if ($class === 'purchases' && in_array($method, array('submit_request','delete_request'), true)) return; // Pembuat Draft invoice boleh membatalkan miliknya sendiri; validasi pemilik // dan status dilakukan lagi dengan row lock di SalesService. if ($class === 'sales' && $method === 'delete_invoice') return; $features = array( 'dashboard'=>'dashboard','approvals'=>'approvals','customers'=>'invoices', 'invoices'=>'invoices','receivables'=>'invoices','salesdocuments'=>'invoices','sales'=>'invoices', 'suppliers'=>'purchases','purchases'=>'purchases','purchasedocuments'=>'purchases','payables'=>'purchases', 'items'=>'items','warehouses'=>'items','kodebarang'=>'items','teknisi'=>'technician_equipment', 'inventoryprofessional'=>'items','asset'=>'asset','fixedassets'=>'asset','lokasiasset'=>'asset', 'cashbank'=>'cash_bank','jurnal'=>'jurnal','basejurnal'=>'jurnal','accounts'=>'jurnal', 'accountingperiods'=>'jurnal','professionalreports'=>'reports','budgets'=>'budgets', 'taxes'=>'taxes','roles'=>'setting','users'=>'setting','employees'=>'employees','generateschedule'=>'employees', 'attendancemonitoring'=>'employees','attendancemonthly'=>'employees','attendancmonthly'=>'employees', 'shifts'=>'employees','holidays'=>'employees','leaverequests'=>'employees','hrpayroll'=>'employees','organizationmaster'=>'employees' ); if (!isset($features[$class])) return; $action = 'can_view'; $isReadMethod = $method==='index'||$method==='list'||$method==='statement'||$method==='detail'||$method==='edit'||strpos($method,'get_')===0||strpos($method,'open_')===0; if (strpos($method,'export')!==false || strpos($method,'pdf')!==false) { $action='can_export'; } elseif (!$isReadMethod && strtoupper($this->input->method()) !== 'GET') { if (strpos($method,'export')!==false) $action='can_export'; elseif (preg_match('/approve|reject|submit|workflow/',$method)) $action='can_approve'; elseif (preg_match('/post|payment|pay|reconcile|final|lock|depreciat|revalue|dispos/',$method)) $action='can_post'; elseif (preg_match('/delete|remove/',$method)) $action='can_delete'; elseif (preg_match('/update|adjust|transfer|return|reverse|cancel/',$method)) $action='can_update'; else $action='can_create'; } if ($class === 'inventoryprofessional') { if ($method === 'submit') $action = 'can_create'; if (in_array($method, array('save_warehouse','save_bin','save_batch','update_reservation'), true)) $action = 'can_update'; if (in_array($method, array('reverse','reconciliation'), true)) $action = 'can_post'; } if ($class === 'fixedassets') { if (in_array($method, array('data','detail','master_data','lookup_inventory','lookup_employees','lookup_journals','opname_detail','report_data','reconciliation','register','qr'), true)) $action = 'can_view'; // save menentukan create/update dari ID di controller; parent cukup // memastikan user dapat melihat modul sebelum pemeriksaan dinamis. if ($method === 'save') $action = 'can_view'; if (in_array($method, array('submit','opname_create'), true)) $action = 'can_create'; if (in_array($method, array('approve','reject'), true)) $action = 'can_approve'; if (in_array($method, array('capitalize','depreciate','mutate','reverse','opname_state'), true)) $action = 'can_post'; if (in_array($method, array('reopen','transfer','responsibility','maintenance','save_category','save_location'), true)) $action = 'can_update'; } if ($class === 'budgets') { if ($method === 'detail') $action = 'can_view'; if ($method === 'save_entry') $action = 'can_view'; if ($method === 'complete_reminder') $action = 'can_update'; if ($method === 'workflow') { $budgetAction = strtolower((string)$this->uri->segment(4)); $action = $budgetAction === 'submit' ? 'can_create' : 'can_approve'; } if (in_array($method, array('save_lines','revise'), true)) $action = 'can_update'; } if ($class === 'sales') { if (in_array($method, array('invoice_detail','delivery_detail','return_detail','lookup_customers','lookup_items','lookup_barcodes','warehouses'), true)) $action = 'can_view'; if (in_array($method, array('append_invoice','delete_invoice','update_delivery'), true)) $action = 'can_update'; if (in_array($method, array('submit_invoice','create_invoice','create_return','submit_return'), true)) $action = 'can_create'; if (in_array($method, array('approve_invoice','reject_invoice','approve_return','reject_return'), true)) $action = 'can_approve'; if (in_array($method, array('post_delivery','payment','receive_return','post_return'), true)) $action = 'can_post'; } if ($class === 'invoices') { if (in_array($method, array('data','lines','lookup_customers','lookup_accounts','lookup_items','lookup_warehouses','lookup_barcodes','detail'), true)) $action = 'can_view'; if ($method === 'create_header') $action = 'can_create'; if (in_array($method, array('add_line','add_lines','remove_line','update_line','update_header','generate_delivery'), true)) $action = 'can_update'; if ($method === 'submit') $action = 'can_submit'; if (in_array($method, array('approve','reject'), true)) $action = 'can_approve'; if ($method === 'delete_draft') $action = 'can_delete'; } if ($class === 'salesdocuments' && $method === 'workflow') { $targetState = strtolower((string)$this->uri->segment(4)); $action = $targetState === 'sent' ? 'can_create' : 'can_approve'; } if ($class === 'items' && in_array($method, array('save_serial_number','delete_serial_number'), true)) $action = 'can_update'; // Teknisi melakukan validasi granular per endpoint di controllernya. // Fallback items menjaga session lama tetap dapat dipakai sampai login ulang. if ($class === 'teknisi' && (is_master_admin_user() || check_permission('technician_equipment','can_view') || check_permission('items','can_view'))) return; $this->requirePermission($features[$class],$action); } protected function rejectUnauthenticatedRequest() { if ($this->input->is_ajax_request()) { $this->output ->set_status_header(401) ->set_content_type('application/json') ->set_output(json_encode([ 'status' => false, 'message' => 'Sesi telah berakhir. Silakan login kembali.' ])) ->_display(); exit; } $this->session->set_flashdata('auth_notice', 'Sesi Anda telah berakhir. Silakan masuk kembali.'); redirect('auth'); exit; } protected function requirePermission($feature, $action = 'can_view') { if (is_master_admin_user()) { return true; } if (!check_permission($feature, $action)) { if ($this->input->is_ajax_request()) { $this->output->set_status_header(403)->set_content_type('application/json','utf-8')->set_output(json_encode(array('status'=>false,'message'=>'Anda tidak memiliki izin untuk tindakan ini.')))->_display(); exit; } show_error('Anda tidak memiliki izin untuk melakukan tindakan ini.', 403, 'Akses ditolak'); } return true; } protected function requireAdmin() { if (!is_master_admin_user()) { show_error('Akses hanya tersedia untuk administrator.', 403, 'Akses ditolak'); } return true; } } class MY_Admin_Controller extends MY_Controller { public function __construct() { parent::__construct(); $this->requireAdmin(); } }