session->userdata('logged_in')) { $this->rejectUnauthenticatedRequest(); } $this->load->library('UserSessionService'); $sessionCheck=$this->usersessionservice->validateCurrent(); if(!$sessionCheck['valid']){ $reason=$sessionCheck['reason']==='expired'?'expired':($sessionCheck['reason']==='device_limit'?'device_limit':'revoked'); $this->terminateAuthenticatedSession($reason); } 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'=>array('hr_employees','employees'),'generateschedule'=>array('hr_schedules','employees'), 'attendancemonitoring'=>array('hr_attendance','employees'),'attendancemonthly'=>array('hr_attendance','employees'),'attendancmonthly'=>array('hr_attendance','employees'), 'shifts'=>array('hr_schedules','employees'),'holidays'=>array('hr_schedules','employees'),'leaverequests'=>array('hr_leave','employees'),'hrpayroll'=>'payroll','payroll'=>'payroll','organizationmaster'=>array('hr_organization','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 === 'leaverequests') { if (in_array($method, array('get_data','detail'), true)) $action = 'can_view'; if ($method === 'save') $action = 'can_view'; if ($method === 'submit') $action = 'can_create'; if (in_array($method, array('approve','reject'), true)) $action = 'can_approve'; if ($method === 'cancel') $action = 'can_view'; } if ($class === 'attendancemonitoring') { if (in_array($method, array('get_data','manual_requests','manual_detail','employees'), true)) $action = 'can_view'; if ($method === 'save_manual') $action = 'can_create'; if ($method === 'submit_manual') $action = 'can_create'; if (in_array($method, array('approve_manual','reject_manual'), true)) $action = 'can_approve'; if ($method === 'cancel_manual') $action = 'can_update'; } if ($class === 'employees') { if (in_array($method,array('getall','detail'),true)) $action='can_view'; if ($method==='store') $action='can_create'; if ($method==='update') $action='can_update'; if ($method==='delete') $action='can_delete'; } if ($class === 'organizationmaster') { if (in_array($method,array('index','data'),true)) $action='can_view'; if (in_array($method,array('save_department','save_position'),true)) $action=$this->input->post('id')?'can_update':'can_create'; if (strpos($method,'delete_')===0) $action='can_delete'; } if ($class === 'generateschedule') { if (in_array($method,array('index','calendar_data','get_employees','detail_schedule'),true)) $action='can_view'; if ($method==='save_manual_schedule') $action='can_create'; if ($method==='update_schedule_shift') $action='can_update'; if ($method==='delete_schedule') $action='can_delete'; } if (in_array($class,array('shifts','holidays'),true)) { if (in_array($method,array('index','get_data','detail'),true)) $action='can_view'; if ($method==='save') $action='can_create'; if ($method==='update') $action='can_update'; if ($method==='delete') $action='can_delete'; } if (in_array($class,array('attendancemonthly','attendancmonthly'),true)) $action='can_view'; if ($class === 'hrpayroll') { if (in_array($method, array('index','period_detail','salary_detail','slip'), true)) $action = 'can_view'; if (strpos($method,'export')!==false) $action = 'can_export'; if (in_array($method, array('create_period','calculate','save_adjustment'), true)) $action = 'can_create'; if (in_array($method, array('component','save_salary_line','deactivate_salary_line','save_position_line','save_attendance_rule','save_settings','deactivate_adjustment'), true)) $action = 'can_update'; if ($method === 'workflow') $action = 'can_approve'; if (in_array($method, array('lock_attendance','post','pay'), true)) $action = 'can_post'; } 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'; // Operasional Perangkat melakukan validasi granular per endpoint di controllernya. // Akses Persediaan tidak otomatis memberikan akses ke modul terpisah ini. if ($class === 'teknisi' && (is_master_admin_user() || check_permission('technician_equipment','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 terminateAuthenticatedSession($reason) { $messages=array( 'expired'=>'Masa login Anda telah berakhir. Silakan login kembali.', 'device_limit'=>'Session dihentikan karena batas perangkat aktif telah tercapai.', 'revoked'=>'Session perangkat ini telah diakhiri. Silakan login kembali.', ); $message=$messages[$reason]??$messages['revoked']; $this->usersessionservice->endCurrent($reason==='expired'?'expired':'revoked',$message); $this->session->sess_destroy(); if($this->input->is_ajax_request()){ $this->output->set_status_header(401)->set_content_type('application/json')->set_output(json_encode(array('status'=>false,'message'=>$message,'redirect'=>base_url('auth'))))->_display(); exit; } redirect('auth?notice='.rawurlencode($reason)); exit; } protected function requirePermission($feature, $action = 'can_view') { if (is_master_admin_user()) { return true; } $features = is_array($feature) ? $feature : array($feature); $allowed = false; foreach ($features as $permissionFeature) { if (check_permission($permissionFeature, $action)) { $allowed = true; break; } } if (!$allowed) { 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(); } }