feat: add role and session access controls
Add configurable role management, expanded project permission coverage, secured module routes, and single-active-session enforcement with online user administration.
This commit is contained in:
parent
b2926d5a0b
commit
c263b9e4ac
|
|
@ -6,10 +6,23 @@ namespace App\Authentication\Authenticators;
|
|||
|
||||
use CodeIgniter\Shield\Authentication\Authenticators\Session;
|
||||
use CodeIgniter\Shield\Authentication\Passwords;
|
||||
use CodeIgniter\Shield\Entities\User;
|
||||
use CodeIgniter\Shield\Result;
|
||||
|
||||
class LegacySafeSession extends Session
|
||||
{
|
||||
public function completeLogin(User $user): void
|
||||
{
|
||||
parent::completeLogin($user);
|
||||
(new \App\Libraries\UserSessionRegistry())->activate((int) $user->id);
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
(new \App\Libraries\UserSessionRegistry())->deactivateCurrent();
|
||||
parent::logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-param array{email?: string, username?: string, password?: string} $credentials
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'title' => 'Developer',
|
||||
'description' => 'Site programmers.',
|
||||
],
|
||||
'doctor' => [
|
||||
'title' => 'Doctor',
|
||||
'description' => 'Clinical doctor access to OPD work, diagnosis reports, and immunization records.',
|
||||
],
|
||||
'pharmacy_admin' => [
|
||||
'title' => 'Pharmacy Admin',
|
||||
'description' => 'Full pharmacy controls including old bill edits and discount overrides.',
|
||||
|
|
@ -139,13 +143,22 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'reports.collection.view' => 'Can view collection reports',
|
||||
'reports.insurance_credit.view' => 'Can view insurance credit reports',
|
||||
'reports.nabh_audit.view' => 'Can view NABH audit report',
|
||||
'reports.billing_operations.view' => 'Can view billing operations reports',
|
||||
'reports.document_issue.view' => 'Can view document issue reports',
|
||||
'finance.workflow.view' => 'Can access Accounts and Finance workflow dashboard',
|
||||
'finance.access' => 'Can access finance procurement and payable workflows',
|
||||
'finance.cash.billing.submit' => 'Can create billing cash entries and submit cash statements',
|
||||
'finance.cash.accounts.accept' => 'Can accept submitted billing cash statements',
|
||||
'finance.cash.accounts.verify' => 'Can verify accepted billing cash statements',
|
||||
'finance.bank.deposit.create' => 'Can create bank deposit register entries',
|
||||
'finance.bank.audit' => 'Can mark bank transaction audit status',
|
||||
'finance.bank.statement.update' => 'Can mark updated in bank statement status',
|
||||
'finance.compliance.view' => 'Can view finance compliance reports',
|
||||
'finance.doctor_payout.manage' => 'Can manage doctor payouts and agreements',
|
||||
'finance.vendor.manage' => 'Can manage finance vendors',
|
||||
'finance.po.manage' => 'Can manage finance purchase orders',
|
||||
'finance.grn.manage' => 'Can manage goods receipt notes',
|
||||
'finance.invoice.manage' => 'Can manage vendor and payable invoices',
|
||||
'diagnosis.access' => 'Can access diagnosis module',
|
||||
'diagnosis.report.view' => 'Can view diagnosis reports',
|
||||
'doctor_work.access' => 'Can access doctor work module',
|
||||
|
|
@ -154,6 +167,9 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'doctor_work.medicine.manage' => 'Can manage OPD medicine',
|
||||
'doctor_work.advice.manage' => 'Can manage OPD advice master',
|
||||
'doctor_work.template_workspace.access' => 'Can access clinical templates workspace',
|
||||
'doctor_work.immunization.access' => 'Can access immunization schedules and patient records',
|
||||
'doctor_work.immunization.schedule-manage' => 'Can synchronize and modify immunization schedule masters',
|
||||
'doctor_work.immunization.record-manage' => 'Can generate and complete patient immunization records',
|
||||
'pharmacy.access' => 'Can access pharmacy module',
|
||||
'pharmacy.invoice.admin' => 'Can fully manage pharmacy invoice controls (old bills and discount overrides)',
|
||||
'pharmacy.invoice.discount.10' => 'Can apply pharmacy invoice discount up to 10%',
|
||||
|
|
@ -184,6 +200,15 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'abdm.taskboard.access' => 'Can access ABDM task board',
|
||||
'abdm.gateway.use' => 'Can run ABDM gateway actions from UI',
|
||||
'abdm.bridge_log.view' => 'Can view ABDM Bridge communication log',
|
||||
'abdm.abha.create' => 'Can create, verify, and link ABHA records for patients',
|
||||
'billing.payment_request.view' => 'Can view organization payment requests',
|
||||
'billing.payment_request.manage' => 'Can process organization payment requests',
|
||||
'billing.refund.view' => 'Can view billing refund requests',
|
||||
'billing.refund.manage' => 'Can process billing refunds',
|
||||
'billing.items.view' => 'Can view billing item masters',
|
||||
'billing.items.manage' => 'Can manage billing item masters',
|
||||
'billing.packages.view' => 'Can view billing package masters',
|
||||
'billing.packages.manage' => 'Can manage billing package masters',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -208,6 +233,8 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'reports.collection.view',
|
||||
'reports.insurance_credit.view',
|
||||
'reports.nabh_audit.view',
|
||||
'reports.billing_operations.view',
|
||||
'reports.document_issue.view',
|
||||
'finance.*',
|
||||
'diagnosis.*',
|
||||
'doctor_work.*',
|
||||
|
|
@ -232,6 +259,8 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'reports.collection.view',
|
||||
'reports.insurance_credit.view',
|
||||
'reports.nabh_audit.view',
|
||||
'reports.billing_operations.view',
|
||||
'reports.document_issue.view',
|
||||
'finance.*',
|
||||
'diagnosis.*',
|
||||
'doctor_work.*',
|
||||
|
|
@ -255,6 +284,8 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'reports.collection.view',
|
||||
'reports.insurance_credit.view',
|
||||
'reports.nabh_audit.view',
|
||||
'reports.billing_operations.view',
|
||||
'reports.document_issue.view',
|
||||
'finance.*',
|
||||
'diagnosis.*',
|
||||
'doctor_work.*',
|
||||
|
|
@ -263,6 +294,14 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'hospital_stock.*',
|
||||
'abdm.*',
|
||||
],
|
||||
'doctor' => [
|
||||
'opd.doctor-panel.access',
|
||||
'doctor_work.access',
|
||||
'doctor_work.immunization.access',
|
||||
'doctor_work.immunization.record-manage',
|
||||
'diagnosis.access',
|
||||
'diagnosis.report.view',
|
||||
],
|
||||
'pharmacy_admin' => [
|
||||
'pharmacy.*',
|
||||
],
|
||||
|
|
@ -299,6 +338,11 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'finance.workflow.view',
|
||||
'finance.cash.billing.submit',
|
||||
'finance.bank.deposit.create',
|
||||
'abdm.abha.create',
|
||||
'billing.payment_request.view',
|
||||
'billing.payment_request.manage',
|
||||
'billing.refund.view',
|
||||
'billing.refund.manage',
|
||||
],
|
||||
'accounts_officer' => [
|
||||
'finance.workflow.view',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Config;
|
||||
|
||||
use App\Filters\SingleActiveSessionFilter;
|
||||
use CodeIgniter\Config\Filters as BaseFilters;
|
||||
use CodeIgniter\Filters\Cors;
|
||||
use CodeIgniter\Filters\CSRF;
|
||||
|
|
@ -34,6 +35,7 @@ class Filters extends BaseFilters
|
|||
'forcehttps' => ForceHTTPS::class,
|
||||
'pagecache' => PageCache::class,
|
||||
'performance' => PerformanceMetrics::class,
|
||||
'singleSession' => SingleActiveSessionFilter::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -71,6 +73,7 @@ class Filters extends BaseFilters
|
|||
*/
|
||||
public array $globals = [
|
||||
'before' => [
|
||||
'singleSession',
|
||||
// 'honeypot',
|
||||
// 'csrf',
|
||||
// 'invalidchars',
|
||||
|
|
|
|||
|
|
@ -333,6 +333,7 @@ $routes->post('Finance/bank_pos_settlement_create', 'Finance::bankPosSettlementC
|
|||
$routes->post('Finance/bank_pos_settlement_accept', 'Finance::bankPosSettlementAccept');
|
||||
|
||||
service('auth')->routes($routes);
|
||||
$routes->get('auth/session-status', 'AuthSession::status');
|
||||
|
||||
// Hospital Stock Management (Storestock)
|
||||
$routes->get('Storestock', 'Storestock::index');
|
||||
|
|
@ -697,14 +698,14 @@ $routes->post('AbdmGateway/share_immunization_bundle', 'AbdmGateway::shareImmuni
|
|||
$routes->post('AbdmGateway/share_wellness_bundle', 'AbdmGateway::shareWellnessBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/share_health_document_bundle', 'AbdmGateway::shareHealthDocumentBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/share_invoice_bundle', 'AbdmGateway::shareInvoiceBundle', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('Immunization', 'Immunization::index', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->get('Immunization/schedule_master', 'Immunization::scheduleMaster', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->post('Immunization/sync_uip_master', 'Immunization::syncUipMaster', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->post('Immunization/update_schedule/(:num)', 'Immunization::updateSchedule/$1', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->get('Immunization/schedule', 'Immunization::schedule', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->get('Immunization/patient/(:num)', 'Immunization::patient/$1', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->post('Immunization/generate_patient_schedule/(:num)', 'Immunization::generatePatientSchedule/$1', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->post('Immunization/complete/(:num)', 'Immunization::complete/$1', ['filter' => 'permission:doctor_work.access,doctor_work.template_workspace.access,abdm.access,abdm.taskboard.access,abdm.gateway.use']);
|
||||
$routes->get('Immunization', 'Immunization::index', ['filter' => 'permission:doctor_work.immunization.access']);
|
||||
$routes->get('Immunization/schedule_master', 'Immunization::scheduleMaster', ['filter' => 'permission:doctor_work.immunization.schedule-manage']);
|
||||
$routes->post('Immunization/sync_uip_master', 'Immunization::syncUipMaster', ['filter' => 'permission:doctor_work.immunization.schedule-manage']);
|
||||
$routes->post('Immunization/update_schedule/(:num)', 'Immunization::updateSchedule/$1', ['filter' => 'permission:doctor_work.immunization.schedule-manage']);
|
||||
$routes->get('Immunization/schedule', 'Immunization::schedule', ['filter' => 'permission:doctor_work.immunization.access']);
|
||||
$routes->get('Immunization/patient/(:num)', 'Immunization::patient/$1', ['filter' => 'permission:doctor_work.immunization.access']);
|
||||
$routes->post('Immunization/generate_patient_schedule/(:num)', 'Immunization::generatePatientSchedule/$1', ['filter' => 'permission:doctor_work.immunization.record-manage']);
|
||||
$routes->post('Immunization/complete/(:num)', 'Immunization::complete/$1', ['filter' => 'permission:doctor_work.immunization.record-manage']);
|
||||
$routes->post('AbdmGateway/push_health_record', 'AbdmGateway::pushHealthRecord', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/health_records_list', 'AbdmGateway::healthRecordsList', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmGateway/bridge_record_status/(:num)', 'AbdmGateway::bridgeRecordStatus/$1', ['filter' => $abdmPermFilter]);
|
||||
|
|
@ -728,15 +729,15 @@ $routes->post('AbdmGateway/abha_aadhaar_verify_otp', 'AbdmGateway::abhaAadhaarVe
|
|||
$routes->post('AbdmGateway/abha_mobile_generate_otp', 'AbdmGateway::abhaMobileGenerateOtp', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmGateway/abha_mobile_verify_otp', 'AbdmGateway::abhaMobileVerifyOtp', ['filter' => $abdmPermFilter]);
|
||||
// ABHA creation wizard — billing/patient ABHA Create tab
|
||||
$routes->post('abha/create/initiate', 'Abha::initiate');
|
||||
$routes->post('abha/create/verify_otp', 'Abha::verifyOtp');
|
||||
$routes->post('abha/create/communication', 'Abha::communication');
|
||||
$routes->post('abha/create/verify_comm_otp', 'Abha::verifyCommOtp');
|
||||
$routes->post('abha/create/confirm_patient', 'Abha::confirmPatient');
|
||||
$routes->post('abha/create/address', 'Abha::address');
|
||||
$routes->get('abha/card/(:segment)', 'Abha::card/$1');
|
||||
$routes->post('abha/register/validate', 'Abha::validateAbha');
|
||||
$routes->get('abha/register/facility_qr', 'Abha::facilityQr');
|
||||
$routes->post('abha/create/initiate', 'Abha::initiate', ['filter' => 'permission:abdm.abha.create']);
|
||||
$routes->post('abha/create/verify_otp', 'Abha::verifyOtp', ['filter' => 'permission:abdm.abha.create']);
|
||||
$routes->post('abha/create/communication', 'Abha::communication', ['filter' => 'permission:abdm.abha.create']);
|
||||
$routes->post('abha/create/verify_comm_otp', 'Abha::verifyCommOtp', ['filter' => 'permission:abdm.abha.create']);
|
||||
$routes->post('abha/create/confirm_patient', 'Abha::confirmPatient', ['filter' => 'permission:abdm.abha.create']);
|
||||
$routes->post('abha/create/address', 'Abha::address', ['filter' => 'permission:abdm.abha.create']);
|
||||
$routes->get('abha/card/(:segment)', 'Abha::card/$1', ['filter' => 'permission:abdm.abha.create']);
|
||||
$routes->post('abha/register/validate', 'Abha::validateAbha', ['filter' => 'permission:abdm.abha.create']);
|
||||
$routes->get('abha/register/facility_qr', 'Abha::facilityQr', ['filter' => 'permission:abdm.abha.create']);
|
||||
$routes->get('AbdmTaskBoard', 'AbdmTaskBoard::index', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmTaskBoard/list', 'AbdmTaskBoard::list', ['filter' => $abdmPermFilter]);
|
||||
$routes->post('AbdmTaskBoard/perform_action', 'AbdmTaskBoard::performAction', ['filter' => $abdmPermFilter]);
|
||||
|
|
@ -925,17 +926,17 @@ $routes->get('Opd_prescription/show_old_Prescribed/(:num)/(:num)', 'Opd_prescrip
|
|||
$routes->get('Opd_prescription/show_medical_item/(:num)', 'Opd_prescription::show_medical_item/$1');
|
||||
$routes->get('Opd_prescription/prescribed_dose/(:num)/(:num)', 'Opd_prescription::prescribed_dose/$1/$2');
|
||||
|
||||
$routes->get('Invoice/list_req_payment', 'Invoice::list_req_payment');
|
||||
$routes->get('Invoice/opdlist', 'Invoice::opdlist');
|
||||
$routes->get('Invoice/chargeslist', 'Invoice::chargeslist');
|
||||
$routes->post('Invoice/getRequestTable', 'Invoice::getRequestTable');
|
||||
$routes->get('Invoice/payment_form/(:num)', 'Invoice::payment_form/$1');
|
||||
$routes->post('Invoice/req_payment_process', 'Invoice::req_payment_process');
|
||||
$routes->get('Invoice/print_org_payment_invoice/(:num)', 'Invoice::print_org_payment_invoice/$1');
|
||||
$routes->get('Invoice/list_refund', 'Invoice::list_refund');
|
||||
$routes->post('Invoice/getRefundTable', 'Invoice::getRefundTable');
|
||||
$routes->get('Invoice/refund_form/(:num)', 'Invoice::refund_form/$1');
|
||||
$routes->post('Invoice/refund_process', 'Invoice::refund_process');
|
||||
$routes->get('Invoice/list_req_payment', 'Invoice::list_req_payment', ['filter' => 'permission:billing.payment_request.view,billing.payment_request.manage']);
|
||||
$routes->get('Invoice/opdlist', 'Invoice::opdlist', ['filter' => 'permission:billing.access,billing.opd.edit']);
|
||||
$routes->get('Invoice/chargeslist', 'Invoice::chargeslist', ['filter' => 'permission:billing.charges.view,billing.access']);
|
||||
$routes->post('Invoice/getRequestTable', 'Invoice::getRequestTable', ['filter' => 'permission:billing.payment_request.view,billing.payment_request.manage']);
|
||||
$routes->get('Invoice/payment_form/(:num)', 'Invoice::payment_form/$1', ['filter' => 'permission:billing.payment_request.view,billing.payment_request.manage']);
|
||||
$routes->post('Invoice/req_payment_process', 'Invoice::req_payment_process', ['filter' => 'permission:billing.payment_request.manage']);
|
||||
$routes->get('Invoice/print_org_payment_invoice/(:num)', 'Invoice::print_org_payment_invoice/$1', ['filter' => 'permission:billing.payment_request.view,billing.payment_request.manage']);
|
||||
$routes->get('Invoice/list_refund', 'Invoice::list_refund', ['filter' => 'permission:billing.refund.view,billing.refund.manage']);
|
||||
$routes->post('Invoice/getRefundTable', 'Invoice::getRefundTable', ['filter' => 'permission:billing.refund.view,billing.refund.manage']);
|
||||
$routes->get('Invoice/refund_form/(:num)', 'Invoice::refund_form/$1', ['filter' => 'permission:billing.refund.view,billing.refund.manage']);
|
||||
$routes->post('Invoice/refund_process', 'Invoice::refund_process', ['filter' => 'permission:billing.refund.manage']);
|
||||
|
||||
$routes->get('Lab_Admin/report_list', 'Setting\Template::report_list');
|
||||
$routes->get('Lab_Admin/report_ultrasound_list/(:num)', 'Setting\Template::report_ultrasound_list/$1');
|
||||
|
|
@ -988,10 +989,10 @@ $routes->post('Opdcase/showfee', 'Opdcase::showfee');
|
|||
$routes->post('Opdcase/confirm_opd', 'Opdcase::confirm_opd');
|
||||
$routes->get('Opdcase/invoice/(:num)', 'Opd::invoice/$1');
|
||||
|
||||
$routes->get('Report', 'Report::index');
|
||||
$routes->get('Report/index', 'Report::index');
|
||||
$routes->get('report', 'Report::index');
|
||||
$routes->get('report/index', 'Report::index');
|
||||
$routes->get('Report', 'Report::index', ['filter' => 'permission:reports.access']);
|
||||
$routes->get('Report/index', 'Report::index', ['filter' => 'permission:reports.access']);
|
||||
$routes->get('report', 'Report::index', ['filter' => 'permission:reports.access']);
|
||||
$routes->get('report/index', 'Report::index', ['filter' => 'permission:reports.access']);
|
||||
|
||||
$reportsCollectionFilter = 'permission:reports.collection.view,reports.access';
|
||||
$routes->get('Report/collection_report', 'Report::collection_report', ['filter' => $reportsCollectionFilter]);
|
||||
|
|
@ -1005,24 +1006,24 @@ $routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segme
|
|||
$routes->get('Report/report_total_payment_total_amount_show/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_total_amount_show/$1/$2/$3', ['filter' => $reportsCollectionFilter]);
|
||||
$routes->get('Report/report_total_payment_total_amount_show/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_total_amount_show/$1/$2/$3/$4', ['filter' => $reportsCollectionFilter]);
|
||||
|
||||
$routes->get('Report/billing_operations_report', 'Report::billing_operations_report');
|
||||
$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)', 'Report::billing_operations_report_data/$1/$2');
|
||||
$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)/(:num)', 'Report::billing_operations_report_data/$1/$2/$3');
|
||||
$routes->get('Report/billing_operations_report', 'Report::billing_operations_report', ['filter' => 'permission:reports.billing_operations.view']);
|
||||
$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)', 'Report::billing_operations_report_data/$1/$2', ['filter' => 'permission:reports.billing_operations.view']);
|
||||
$routes->get('Report/billing_operations_report_data/(:segment)/(:segment)/(:num)', 'Report::billing_operations_report_data/$1/$2/$3', ['filter' => 'permission:reports.billing_operations.view']);
|
||||
|
||||
$routes->get('Report/document_list', 'Report::document_list');
|
||||
$routes->get('Report/document_list_data/(:segment)/(:num)', 'Report::document_list_data/$1/$2');
|
||||
$routes->get('Report/document_list_data/(:segment)/(:num)/(:num)', 'Report::document_list_data/$1/$2/$3');
|
||||
$routes->get('Report/document_list', 'Report::document_list', ['filter' => 'permission:reports.document_issue.view']);
|
||||
$routes->get('Report/document_list_data/(:segment)/(:num)', 'Report::document_list_data/$1/$2', ['filter' => 'permission:reports.document_issue.view']);
|
||||
$routes->get('Report/document_list_data/(:segment)/(:num)/(:num)', 'Report::document_list_data/$1/$2/$3', ['filter' => 'permission:reports.document_issue.view']);
|
||||
|
||||
// Legacy Report5 compatibility for Document Issue Report.
|
||||
$routes->get('Report5/document_list', 'Report::document_list');
|
||||
$routes->get('Report5/document_list_data/(:segment)/(:num)', 'Report::document_list_data/$1/$2');
|
||||
$routes->get('Report5/document_list_data/(:segment)/(:num)/(:num)', 'Report::document_list_data/$1/$2/$3');
|
||||
$routes->get('Report5/document_list', 'Report::document_list', ['filter' => 'permission:reports.document_issue.view']);
|
||||
$routes->get('Report5/document_list_data/(:segment)/(:num)', 'Report::document_list_data/$1/$2', ['filter' => 'permission:reports.document_issue.view']);
|
||||
$routes->get('Report5/document_list_data/(:segment)/(:num)/(:num)', 'Report::document_list_data/$1/$2/$3', ['filter' => 'permission:reports.document_issue.view']);
|
||||
|
||||
$routes->get('Report/diagnosis_report', 'Report::diagnosis_report', ['filter' => 'permission:diagnosis.report.view,diagnosis.access']);
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)', 'Report::diagnosis_report_data/$1');
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2');
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2/$3');
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::diagnosis_report_data/$1/$2/$3/$4');
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)', 'Report::diagnosis_report_data/$1', ['filter' => 'permission:diagnosis.report.view,diagnosis.access']);
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2', ['filter' => 'permission:diagnosis.report.view,diagnosis.access']);
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)/(:segment)', 'Report::diagnosis_report_data/$1/$2/$3', ['filter' => 'permission:diagnosis.report.view,diagnosis.access']);
|
||||
$routes->get('Report/diagnosis_report_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::diagnosis_report_data/$1/$2/$3/$4', ['filter' => 'permission:diagnosis.report.view,diagnosis.access']);
|
||||
|
||||
$reportsNabhFilter = 'permission:reports.nabh_audit.view,reports.access';
|
||||
$routes->get('Report/nabh_audit_report', 'Report::nabh_audit_report', ['filter' => $reportsNabhFilter]);
|
||||
|
|
@ -1049,12 +1050,12 @@ $routes->get('Report/ayushman_case_dashboard_data/(:segment)/(:segment)', 'Repor
|
|||
$routes->get('Report/ayushman_case_dashboard_data/(:segment)/(:segment)/(:num)', 'Report::ayushman_case_dashboard_data/$1/$2/$3', ['filter' => $reportsInsuranceFilter]);
|
||||
|
||||
// Legacy Report4 compatibility for Insurance Case reports
|
||||
$routes->get('Report4/echs_ipd_list_main', 'Report::echs_ipd_list_main');
|
||||
$routes->get('Report4/echs_opd_list_main', 'Report::echs_opd_list_main');
|
||||
$routes->get('Report4/echs_ipd_list_data/(:segment)/(:segment)/(:segment)', 'Report::echs_ipd_list_data/$1/$2/$3');
|
||||
$routes->get('Report4/echs_ipd_list_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::echs_ipd_list_data/$1/$2/$3/$4');
|
||||
$routes->get('Report4/echs_opd_list_data/(:segment)/(:segment)/(:segment)', 'Report::echs_opd_list_data/$1/$2/$3');
|
||||
$routes->get('Report4/echs_opd_list_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::echs_opd_list_data/$1/$2/$3/$4');
|
||||
$routes->get('Report4/echs_ipd_list_main', 'Report::echs_ipd_list_main', ['filter' => $reportsInsuranceFilter]);
|
||||
$routes->get('Report4/echs_opd_list_main', 'Report::echs_opd_list_main', ['filter' => $reportsInsuranceFilter]);
|
||||
$routes->get('Report4/echs_ipd_list_data/(:segment)/(:segment)/(:segment)', 'Report::echs_ipd_list_data/$1/$2/$3', ['filter' => $reportsInsuranceFilter]);
|
||||
$routes->get('Report4/echs_ipd_list_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::echs_ipd_list_data/$1/$2/$3/$4', ['filter' => $reportsInsuranceFilter]);
|
||||
$routes->get('Report4/echs_opd_list_data/(:segment)/(:segment)/(:segment)', 'Report::echs_opd_list_data/$1/$2/$3', ['filter' => $reportsInsuranceFilter]);
|
||||
$routes->get('Report4/echs_opd_list_data/(:segment)/(:segment)/(:segment)/(:num)', 'Report::echs_opd_list_data/$1/$2/$3/$4', ['filter' => $reportsInsuranceFilter]);
|
||||
|
||||
// Organization Packing Routes
|
||||
$routes->get('org-packing', 'OrgPacking::index');
|
||||
|
|
@ -1195,15 +1196,25 @@ $routes->group('setting', static function($routes) {
|
|||
$settingsBedFilter = 'permission:settings.bed_status.view,admin.settings,admin.access';
|
||||
$settingsChargesFilter = 'permission:settings.charges.access,admin.settings,admin.access';
|
||||
$routes->get('admin', 'Setting\\Admin::index', ['filter' => $settingsAdminFilter]);
|
||||
$routes->get('admin/user-management', 'Setting\\UserManagement::index');
|
||||
$routes->get('admin/user-management/new', 'Setting\\UserManagement::create');
|
||||
$routes->post('admin/user-management/new', 'Setting\\UserManagement::store');
|
||||
$routes->get('admin/user-management/edit/(:num)', 'Setting\\UserManagement::edit/$1');
|
||||
$routes->post('admin/user-management/edit/(:num)', 'Setting\\UserManagement::update/$1');
|
||||
$routes->get('admin/user-management/reset-password/(:num)', 'Setting\\UserManagement::resetPasswordForm/$1');
|
||||
$routes->post('admin/user-management/reset-password/(:num)', 'Setting\\UserManagement::resetPassword/$1');
|
||||
$routes->get('admin/user-management/permissions', 'Setting\\UserManagement::permissions');
|
||||
$routes->post('admin/user-management/permissions', 'Setting\\UserManagement::savePermissions');
|
||||
$routes->get('admin/user-management', 'Setting\\UserManagement::index', ['filter' => $settingsAdminFilter]);
|
||||
$routes->get('admin/user-management/new', 'Setting\\UserManagement::create', ['filter' => 'permission:users.create,users.manage-admins']);
|
||||
$routes->post('admin/user-management/new', 'Setting\\UserManagement::store', ['filter' => ['permission:users.create,users.manage-admins', 'csrf']]);
|
||||
$routes->get('admin/user-management/edit/(:num)', 'Setting\\UserManagement::edit/$1', ['filter' => 'permission:users.edit,users.manage-admins']);
|
||||
$routes->post('admin/user-management/edit/(:num)', 'Setting\\UserManagement::update/$1', ['filter' => ['permission:users.edit,users.manage-admins', 'csrf']]);
|
||||
$routes->get('admin/user-management/reset-password/(:num)', 'Setting\\UserManagement::resetPasswordForm/$1', ['filter' => 'permission:users.edit,users.manage-admins']);
|
||||
$routes->post('admin/user-management/reset-password/(:num)', 'Setting\\UserManagement::resetPassword/$1', ['filter' => ['permission:users.edit,users.manage-admins', 'csrf']]);
|
||||
$routes->get('admin/user-management/permissions', 'Setting\\UserManagement::permissions', ['filter' => 'permission:users.manage-admins']);
|
||||
$routes->post('admin/user-management/permissions', 'Setting\\UserManagement::savePermissions', ['filter' => ['permission:users.manage-admins', 'csrf']]);
|
||||
$routes->get('admin/user-management/sessions', 'Setting\\UserManagement::sessions', ['filter' => 'permission:users.edit,users.manage-admins']);
|
||||
$routes->post('admin/user-management/sessions/force-logout/(:num)', 'Setting\\UserManagement::forceLogout/$1', ['filter' => ['permission:users.edit,users.manage-admins', 'csrf']]);
|
||||
$routes->post('admin/user-management/delete/(:num)', 'Setting\\UserManagement::delete/$1', ['filter' => ['permission:users.delete,users.manage-admins', 'csrf']]);
|
||||
$routes->get('admin/roles', 'Setting\\RoleManagement::index', ['filter' => 'permission:users.manage-admins']);
|
||||
$routes->get('admin/roles/new', 'Setting\\RoleManagement::create', ['filter' => 'permission:users.manage-admins']);
|
||||
$routes->post('admin/roles', 'Setting\\RoleManagement::store', ['filter' => ['permission:users.manage-admins', 'csrf']]);
|
||||
$routes->get('admin/roles/edit/(:num)', 'Setting\\RoleManagement::edit/$1', ['filter' => 'permission:users.manage-admins']);
|
||||
$routes->post('admin/roles/edit/(:num)', 'Setting\\RoleManagement::update/$1', ['filter' => ['permission:users.manage-admins', 'csrf']]);
|
||||
$routes->post('admin/roles/status/(:num)', 'Setting\\RoleManagement::status/$1', ['filter' => ['permission:users.manage-admins', 'csrf']]);
|
||||
$routes->post('admin/roles/delete/(:num)', 'Setting\\RoleManagement::delete/$1', ['filter' => ['permission:users.manage-admins', 'csrf']]);
|
||||
$routes->get('admin/ai-settings', 'Setting\\AiSettings::index');
|
||||
$routes->post('admin/ai-settings/save', 'Setting\\AiSettings::save');
|
||||
$routes->post('admin/ai-settings/test', 'Setting\\AiSettings::test');
|
||||
|
|
@ -1323,7 +1334,7 @@ $routes->group('setting', static function($routes) {
|
|||
$routes->get('charges', 'Setting\\Charges::index', ['filter' => $settingsChargesFilter]);
|
||||
});
|
||||
|
||||
$routes->group('item', static function($routes) {
|
||||
$routes->group('item', ['filter' => 'permission:billing.items.view,billing.items.manage,billing.access,settings.charges.access,admin.settings'], static function($routes) {
|
||||
$routes->get('search', 'Item::search');
|
||||
$routes->get('search-itemtype', 'Item::searchItemType');
|
||||
$routes->get('search-adv/(:num)', 'Item::searchAdv/$1');
|
||||
|
|
@ -1331,17 +1342,17 @@ $routes->group('item', static function($routes) {
|
|||
$routes->get('export-excel/(:num)', 'Item::exportExcel/$1');
|
||||
$routes->match(['GET', 'POST'], 'item-record/(:num)', 'Item::itemRecord/$1');
|
||||
$routes->get('itemtype-record/(:num)', 'Item::itemTypeRecord/$1');
|
||||
$routes->get('add', 'Item::addRecord');
|
||||
$routes->get('add-type', 'Item::addItemTypeRecord');
|
||||
$routes->post('create', 'Item::createRecord');
|
||||
$routes->post('update', 'Item::updateRecord');
|
||||
$routes->post('create-type', 'Item::createItemTypeRecord');
|
||||
$routes->post('update-type', 'Item::updateItemTypeRecord');
|
||||
$routes->post('insurance/add', 'Item::addInsuranceItemRecord');
|
||||
$routes->post('insurance/remove', 'Item::removeInsuranceItem');
|
||||
$routes->get('add', 'Item::addRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->get('add-type', 'Item::addItemTypeRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('create', 'Item::createRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('update', 'Item::updateRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('create-type', 'Item::createItemTypeRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('update-type', 'Item::updateItemTypeRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('insurance/add', 'Item::addInsuranceItemRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('insurance/remove', 'Item::removeInsuranceItem', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
});
|
||||
|
||||
$routes->group('item-ipd', static function($routes) {
|
||||
$routes->group('item-ipd', ['filter' => 'permission:billing.items.view,billing.items.manage,billing.access,settings.charges.access,admin.settings'], static function($routes) {
|
||||
$routes->get('search', 'ItemIpd::search');
|
||||
$routes->get('search-itemtype', 'ItemIpd::searchItemType');
|
||||
$routes->get('search-adv/(:num)', 'ItemIpd::searchAdv/$1');
|
||||
|
|
@ -1349,19 +1360,19 @@ $routes->group('item-ipd', static function($routes) {
|
|||
$routes->get('export-excel/(:num)', 'ItemIpd::exportExcel/$1');
|
||||
$routes->match(['GET', 'POST'], 'item-record/(:num)', 'ItemIpd::itemRecord/$1');
|
||||
$routes->get('itemtype-record/(:num)', 'ItemIpd::itemTypeRecord/$1');
|
||||
$routes->get('add', 'ItemIpd::addRecord');
|
||||
$routes->get('add-type', 'ItemIpd::addItemTypeRecord');
|
||||
$routes->post('create', 'ItemIpd::createRecord');
|
||||
$routes->post('update', 'ItemIpd::updateRecord');
|
||||
$routes->post('create-type', 'ItemIpd::createItemTypeRecord');
|
||||
$routes->post('update-type', 'ItemIpd::updateItemTypeRecord');
|
||||
$routes->post('delete-type', 'ItemIpd::deleteItemTypeRecord');
|
||||
$routes->post('change-sort-type', 'ItemIpd::changeItemTypeSort');
|
||||
$routes->post('insurance/add', 'ItemIpd::addInsuranceItemRecord');
|
||||
$routes->post('insurance/remove', 'ItemIpd::removeInsuranceItem');
|
||||
$routes->get('add', 'ItemIpd::addRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->get('add-type', 'ItemIpd::addItemTypeRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('create', 'ItemIpd::createRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('update', 'ItemIpd::updateRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('create-type', 'ItemIpd::createItemTypeRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('update-type', 'ItemIpd::updateItemTypeRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('delete-type', 'ItemIpd::deleteItemTypeRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('change-sort-type', 'ItemIpd::changeItemTypeSort', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('insurance/add', 'ItemIpd::addInsuranceItemRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('insurance/remove', 'ItemIpd::removeInsuranceItem', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
});
|
||||
|
||||
$routes->group('package', static function($routes) {
|
||||
$routes->group('package', ['filter' => 'permission:billing.packages.view,billing.packages.manage,billing.access,settings.charges.access,admin.settings'], static function($routes) {
|
||||
$routes->get('search', 'Package::search');
|
||||
$routes->get('search-itemtype', 'Package::searchItemType');
|
||||
$routes->get('search-adv/(:num)', 'Package::searchAdv/$1');
|
||||
|
|
@ -1369,17 +1380,17 @@ $routes->group('package', static function($routes) {
|
|||
$routes->get('export-excel/(:num)', 'Package::exportExcel/$1');
|
||||
$routes->match(['GET', 'POST'], 'item-record/(:num)', 'Package::itemRecord/$1');
|
||||
$routes->get('itemtype-record/(:num)', 'Package::itemTypeRecord/$1');
|
||||
$routes->get('add', 'Package::addRecord');
|
||||
$routes->get('add-type', 'Package::addItemTypeRecord');
|
||||
$routes->post('create', 'Package::createRecord');
|
||||
$routes->post('update', 'Package::updateRecord');
|
||||
$routes->post('create-type', 'Package::createItemTypeRecord');
|
||||
$routes->post('update-type', 'Package::updateItemTypeRecord');
|
||||
$routes->post('insurance/add', 'Package::addInsuranceItemRecord');
|
||||
$routes->post('insurance/remove', 'Package::removeInsuranceItem');
|
||||
$routes->get('add', 'Package::addRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->get('add-type', 'Package::addItemTypeRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('create', 'Package::createRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('update', 'Package::updateRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('create-type', 'Package::createItemTypeRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('update-type', 'Package::updateItemTypeRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('insurance/add', 'Package::addInsuranceItemRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('insurance/remove', 'Package::removeInsuranceItem', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
});
|
||||
|
||||
$routes->group('Package', static function($routes) {
|
||||
$routes->group('Package', ['filter' => 'permission:billing.packages.view,billing.packages.manage,billing.access,settings.charges.access,admin.settings'], static function($routes) {
|
||||
$routes->get('search', 'Package::search');
|
||||
$routes->get('search_itemtype', 'Package::searchItemType');
|
||||
$routes->get('search_adv/(:num)', 'Package::searchAdv/$1');
|
||||
|
|
@ -1387,17 +1398,17 @@ $routes->group('Package', static function($routes) {
|
|||
$routes->get('export-excel/(:num)', 'Package::exportExcel/$1');
|
||||
$routes->match(['GET', 'POST'], 'item_record/(:num)', 'Package::itemRecord/$1');
|
||||
$routes->get('itemtype_record/(:num)', 'Package::itemTypeRecord/$1');
|
||||
$routes->get('AddRecord', 'Package::addRecord');
|
||||
$routes->get('AddItemTypeRecord', 'Package::addItemTypeRecord');
|
||||
$routes->post('CreateRecord', 'Package::createRecord');
|
||||
$routes->post('UpdateRecord', 'Package::updateRecord');
|
||||
$routes->post('CreateItemTypeRecord', 'Package::createItemTypeRecord');
|
||||
$routes->post('UpdateItemTypeRecord', 'Package::updateItemTypeRecord');
|
||||
$routes->post('AddInsuranceItemRecord', 'Package::addInsuranceItemRecord');
|
||||
$routes->post('remove_record_item', 'Package::removeInsuranceItem');
|
||||
$routes->get('AddRecord', 'Package::addRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->get('AddItemTypeRecord', 'Package::addItemTypeRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('CreateRecord', 'Package::createRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('UpdateRecord', 'Package::updateRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('CreateItemTypeRecord', 'Package::createItemTypeRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('UpdateItemTypeRecord', 'Package::updateItemTypeRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('AddInsuranceItemRecord', 'Package::addInsuranceItemRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('remove_record_item', 'Package::removeInsuranceItem', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
});
|
||||
|
||||
$routes->group('IPD_Package', static function($routes) {
|
||||
$routes->group('IPD_Package', ['filter' => 'permission:billing.packages.view,billing.packages.manage,billing.access,settings.charges.access,admin.settings'], static function($routes) {
|
||||
$routes->get('search', 'Package::search');
|
||||
$routes->get('search_itemtype', 'Package::searchItemType');
|
||||
$routes->get('search_adv/(:num)', 'Package::searchAdv/$1');
|
||||
|
|
@ -1405,17 +1416,17 @@ $routes->group('IPD_Package', static function($routes) {
|
|||
$routes->get('export-excel/(:num)', 'Package::exportExcel/$1');
|
||||
$routes->match(['GET', 'POST'], 'item_record/(:num)', 'Package::itemRecord/$1');
|
||||
$routes->get('itemtype_record/(:num)', 'Package::itemTypeRecord/$1');
|
||||
$routes->get('AddRecord', 'Package::addRecord');
|
||||
$routes->get('AddItemTypeRecord', 'Package::addItemTypeRecord');
|
||||
$routes->post('CreateRecord', 'Package::createRecord');
|
||||
$routes->post('UpdateRecord', 'Package::updateRecord');
|
||||
$routes->post('CreateItemTypeRecord', 'Package::createItemTypeRecord');
|
||||
$routes->post('UpdateItemTypeRecord', 'Package::updateItemTypeRecord');
|
||||
$routes->post('AddInsuranceItemRecord', 'Package::addInsuranceItemRecord');
|
||||
$routes->post('remove_record_item', 'Package::removeInsuranceItem');
|
||||
$routes->get('AddRecord', 'Package::addRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->get('AddItemTypeRecord', 'Package::addItemTypeRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('CreateRecord', 'Package::createRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('UpdateRecord', 'Package::updateRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('CreateItemTypeRecord', 'Package::createItemTypeRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('UpdateItemTypeRecord', 'Package::updateItemTypeRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('AddInsuranceItemRecord', 'Package::addInsuranceItemRecord', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
$routes->post('remove_record_item', 'Package::removeInsuranceItem', ['filter' => 'permission:billing.packages.manage,admin.settings']);
|
||||
});
|
||||
|
||||
$routes->group('Item_IPD', static function($routes) {
|
||||
$routes->group('Item_IPD', ['filter' => 'permission:billing.items.view,billing.items.manage,billing.access,settings.charges.access,admin.settings'], static function($routes) {
|
||||
$routes->get('search', 'ItemIpd::search');
|
||||
$routes->get('search_itemtype', 'ItemIpd::searchItemType');
|
||||
$routes->get('search_adv/(:num)', 'ItemIpd::searchAdv/$1');
|
||||
|
|
@ -1423,13 +1434,13 @@ $routes->group('Item_IPD', static function($routes) {
|
|||
$routes->get('export-excel/(:num)', 'ItemIpd::exportExcel/$1');
|
||||
$routes->match(['GET', 'POST'], 'item_record/(:num)', 'ItemIpd::itemRecord/$1');
|
||||
$routes->get('itemtype_record/(:num)', 'ItemIpd::itemTypeRecord/$1');
|
||||
$routes->get('AddRecord', 'ItemIpd::addRecord');
|
||||
$routes->get('AddItemTypeRecord', 'ItemIpd::addItemTypeRecord');
|
||||
$routes->post('CreateRecord', 'ItemIpd::createRecord');
|
||||
$routes->post('UpdateRecord', 'ItemIpd::updateRecord');
|
||||
$routes->post('CreateItemTypeRecord', 'ItemIpd::createItemTypeRecord');
|
||||
$routes->post('UpdateItemTypeRecord', 'ItemIpd::updateItemTypeRecord');
|
||||
$routes->post('change_sort_itemtype', 'ItemIpd::changeItemTypeSort');
|
||||
$routes->post('AddInsuranceItemRecord', 'ItemIpd::addInsuranceItemRecord');
|
||||
$routes->post('remove_record_item', 'ItemIpd::removeInsuranceItem');
|
||||
$routes->get('AddRecord', 'ItemIpd::addRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->get('AddItemTypeRecord', 'ItemIpd::addItemTypeRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('CreateRecord', 'ItemIpd::createRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('UpdateRecord', 'ItemIpd::updateRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('CreateItemTypeRecord', 'ItemIpd::createItemTypeRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('UpdateItemTypeRecord', 'ItemIpd::updateItemTypeRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('change_sort_itemtype', 'ItemIpd::changeItemTypeSort', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('AddInsuranceItemRecord', 'ItemIpd::addInsuranceItemRecord', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
$routes->post('remove_record_item', 'ItemIpd::removeInsuranceItem', ['filter' => 'permission:billing.items.manage,admin.settings']);
|
||||
});
|
||||
|
|
|
|||
21
app/Controllers/AuthSession.php
Normal file
21
app/Controllers/AuthSession.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
class AuthSession extends BaseController
|
||||
{
|
||||
public function status()
|
||||
{
|
||||
if (! function_exists('auth') || ! auth()->loggedIn()) {
|
||||
return $this->response->setStatusCode(401)->setJSON([
|
||||
'active' => false,
|
||||
'login_url' => base_url('login'),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'active' => true,
|
||||
'checked_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1794,6 +1794,13 @@ class Finance extends BaseController
|
|||
'finance.bank.deposit.create',
|
||||
'finance.bank.audit',
|
||||
'finance.bank.statement.update',
|
||||
'finance.access',
|
||||
'finance.compliance.view',
|
||||
'finance.doctor_payout.manage',
|
||||
'finance.vendor.manage',
|
||||
'finance.po.manage',
|
||||
'finance.grn.manage',
|
||||
'finance.invoice.manage',
|
||||
])) {
|
||||
return $this->response->setStatusCode(403)->setBody('Access denied');
|
||||
}
|
||||
|
|
|
|||
132
app/Controllers/Setting/RoleManagement.php
Normal file
132
app/Controllers/Setting/RoleManagement.php
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers\Setting;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\RoleRegistry;
|
||||
use InvalidArgumentException;
|
||||
use Throwable;
|
||||
|
||||
class RoleManagement extends BaseController
|
||||
{
|
||||
public function index(): string
|
||||
{
|
||||
return $this->listView();
|
||||
}
|
||||
|
||||
public function create(): string
|
||||
{
|
||||
return $this->formView();
|
||||
}
|
||||
|
||||
public function store(): string
|
||||
{
|
||||
return $this->save(null);
|
||||
}
|
||||
|
||||
public function edit(int $roleId): string
|
||||
{
|
||||
$role = (new RoleRegistry($this->db))->find($roleId);
|
||||
if ($role === null) {
|
||||
return $this->listView(null, ['Role not found.']);
|
||||
}
|
||||
|
||||
return $this->formView($role);
|
||||
}
|
||||
|
||||
public function update(int $roleId): string
|
||||
{
|
||||
return $this->save($roleId);
|
||||
}
|
||||
|
||||
public function status(int $roleId)
|
||||
{
|
||||
$registry = new RoleRegistry($this->db);
|
||||
$role = $registry->find($roleId);
|
||||
try {
|
||||
if ($role === null) {
|
||||
throw new InvalidArgumentException('Role not found.');
|
||||
}
|
||||
$active = (int) $this->request->getPost('is_active') === 1;
|
||||
$registry->setActive($roleId, $active, $this->currentUserId());
|
||||
$this->auditClinicalUpdate('role_master', 'status', $roleId, ['is_active' => (int) $role['is_active']], ['is_active' => $active ? 1 : 0]);
|
||||
|
||||
return $this->response->setJSON(['update' => 1, 'message' => $active ? 'Role activated.' : 'Role deactivated.', 'csrf_hash' => csrf_hash()]);
|
||||
} catch (Throwable $e) {
|
||||
return $this->response->setStatusCode(422)->setJSON(['update' => 0, 'error_text' => $e->getMessage(), 'csrf_hash' => csrf_hash()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(int $roleId)
|
||||
{
|
||||
$registry = new RoleRegistry($this->db);
|
||||
$role = $registry->find($roleId);
|
||||
try {
|
||||
if ($role === null) {
|
||||
throw new InvalidArgumentException('Role not found.');
|
||||
}
|
||||
$registry->delete($roleId);
|
||||
$this->auditClinicalUpdate('role_master', 'delete', $roleId, $role, ['deleted' => true]);
|
||||
|
||||
return $this->response->setJSON(['update' => 1, 'message' => 'Role deleted.', 'csrf_hash' => csrf_hash()]);
|
||||
} catch (Throwable $e) {
|
||||
return $this->response->setStatusCode(422)->setJSON(['update' => 0, 'error_text' => $e->getMessage(), 'csrf_hash' => csrf_hash()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function save(?int $roleId): string
|
||||
{
|
||||
$registry = new RoleRegistry($this->db);
|
||||
$oldRole = $roleId ? $registry->find($roleId) : null;
|
||||
$input = [
|
||||
'alias' => $oldRole['alias'] ?? $this->request->getPost('alias'),
|
||||
'title' => $this->request->getPost('title'),
|
||||
'description' => $this->request->getPost('description'),
|
||||
'permissions' => (array) $this->request->getPost('permissions'),
|
||||
];
|
||||
|
||||
try {
|
||||
$savedId = $registry->save($roleId, $input, $this->currentUserId());
|
||||
$newRole = $registry->find($savedId);
|
||||
$this->auditClinicalUpdate('role_master', $roleId ? 'update' : 'create', $savedId, $oldRole, $newRole);
|
||||
|
||||
return $this->listView($roleId ? 'Role updated successfully.' : 'Role created successfully.');
|
||||
} catch (Throwable $e) {
|
||||
$role = array_merge($oldRole ?? [], $input);
|
||||
return $this->formView($role, [$e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function listView(?string $message = null, array $errors = []): string
|
||||
{
|
||||
return view('Setting/Admin/RoleManagement/index', [
|
||||
'roles' => (new RoleRegistry($this->db))->all(),
|
||||
'message' => $message,
|
||||
'errors' => $errors,
|
||||
]);
|
||||
}
|
||||
|
||||
private function formView(?array $role = null, array $errors = []): string
|
||||
{
|
||||
$permissions = (array) setting('AuthGroups.permissions');
|
||||
$wildcards = [];
|
||||
foreach (array_keys($permissions) as $permission) {
|
||||
$scope = strstr($permission, '.', true);
|
||||
if ($scope !== false) {
|
||||
$wildcards[$scope . '.*'] = 'All ' . ucwords(str_replace('_', ' ', $scope)) . ' permissions';
|
||||
}
|
||||
}
|
||||
|
||||
return view('Setting/Admin/RoleManagement/form', [
|
||||
'role' => $role,
|
||||
'permissions' => $permissions,
|
||||
'wildcards' => $wildcards,
|
||||
'errors' => $errors,
|
||||
]);
|
||||
}
|
||||
|
||||
private function currentUserId(): int
|
||||
{
|
||||
return function_exists('auth') ? (int) (auth()->user()->id ?? 0) : 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,39 @@
|
|||
namespace App\Controllers\Setting;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Libraries\UserSessionRegistry;
|
||||
use CodeIgniter\Shield\Entities\User;
|
||||
use CodeIgniter\Shield\Models\UserModel;
|
||||
|
||||
class UserManagement extends BaseController
|
||||
{
|
||||
private function currentUserId(): int
|
||||
{
|
||||
return function_exists('auth') ? (int) (auth()->user()->id ?? 0) : 0;
|
||||
}
|
||||
|
||||
private function canManageAdmins(): bool
|
||||
{
|
||||
return function_exists('auth') && auth()->loggedIn() && auth()->user()->can('users.manage-admins');
|
||||
}
|
||||
|
||||
private function availableRoles(): array
|
||||
{
|
||||
$roles = (array) setting('AuthGroups.groups');
|
||||
if (! $this->canManageAdmins()) {
|
||||
foreach (['superadmin', 'admin', 'developer'] as $privilegedRole) {
|
||||
unset($roles[$privilegedRole]);
|
||||
}
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
private function isPrivilegedUser(User $user): bool
|
||||
{
|
||||
return $user->inGroup('superadmin', 'admin', 'developer') || $user->can('users.manage-admins');
|
||||
}
|
||||
|
||||
public function index(): string
|
||||
{
|
||||
$userModel = model(UserModel::class);
|
||||
|
|
@ -25,23 +53,25 @@ class UserManagement extends BaseController
|
|||
public function create(): string
|
||||
{
|
||||
return view('Setting/Admin/UserManagement/create', [
|
||||
'roles' => setting('AuthGroups.groups'),
|
||||
'roles' => $this->availableRoles(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$rules = [
|
||||
'username' => 'required|min_length[3]|max_length[30]',
|
||||
'username' => 'required|min_length[3]|max_length[30]|regex_match[/^[a-zA-Z0-9.]+$/]',
|
||||
'email' => 'required|valid_email|max_length[254]',
|
||||
'password' => 'required|min_length[8]',
|
||||
'role' => 'required',
|
||||
'person_name' => 'permit_empty|max_length[120]',
|
||||
'phone_no' => 'permit_empty|regex_match[/^[0-9+()\\s-]{7,20}$/]',
|
||||
];
|
||||
|
||||
if (! $this->validate($rules)) {
|
||||
if ($this->request->isAJAX()) {
|
||||
return view('Setting/Admin/UserManagement/create', [
|
||||
'roles' => setting('AuthGroups.groups'),
|
||||
'roles' => $this->availableRoles(),
|
||||
'errors' => $this->validator->getErrors(),
|
||||
'formData' => $this->request->getPost(),
|
||||
]);
|
||||
|
|
@ -51,12 +81,12 @@ class UserManagement extends BaseController
|
|||
}
|
||||
|
||||
$role = strtolower((string) $this->request->getPost('role'));
|
||||
$allowedRoles = array_keys(setting('AuthGroups.groups'));
|
||||
$allowedRoles = array_keys($this->availableRoles());
|
||||
if (! in_array($role, $allowedRoles, true)) {
|
||||
$errors = ['role' => 'Invalid role selected.'];
|
||||
if ($this->request->isAJAX()) {
|
||||
return view('Setting/Admin/UserManagement/create', [
|
||||
'roles' => setting('AuthGroups.groups'),
|
||||
'roles' => $this->availableRoles(),
|
||||
'errors' => $errors,
|
||||
'formData' => $this->request->getPost(),
|
||||
]);
|
||||
|
|
@ -72,11 +102,31 @@ class UserManagement extends BaseController
|
|||
$user->active = 1;
|
||||
|
||||
$userModel = model(UserModel::class);
|
||||
$userModel->save($user);
|
||||
try {
|
||||
if (! $userModel->save($user)) {
|
||||
throw new \RuntimeException(implode(' ', $userModel->errors()));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return view('Setting/Admin/UserManagement/create', [
|
||||
'roles' => $this->availableRoles(),
|
||||
'errors' => ['user' => $e->getMessage() !== '' ? $e->getMessage() : 'Unable to create user.'],
|
||||
'formData' => $this->request->getPost(),
|
||||
]);
|
||||
}
|
||||
|
||||
$savedUser = $userModel->find($userModel->getInsertID());
|
||||
if ($savedUser !== null) {
|
||||
$savedUser->addGroup($role);
|
||||
$this->syncIdentityMeta(
|
||||
(int) $savedUser->id,
|
||||
trim((string) $this->request->getPost('person_name')),
|
||||
trim((string) $this->request->getPost('phone_no'))
|
||||
);
|
||||
$this->auditClinicalUpdate('user_management', 'create', $savedUser->id, null, [
|
||||
'username' => $savedUser->username,
|
||||
'role' => $role,
|
||||
'active' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->request->isAJAX()) {
|
||||
|
|
@ -111,10 +161,14 @@ class UserManagement extends BaseController
|
|||
$selectedUser = $userModel->withPermissions()->find($selectedUserId);
|
||||
}
|
||||
|
||||
$roleContext = $this->rolePermissionContext($selectedUser);
|
||||
|
||||
return view('Setting/Admin/UserManagement/permissions', [
|
||||
'users' => $users,
|
||||
'permissions' => setting('AuthGroups.permissions'),
|
||||
'selectedUser' => $selectedUser,
|
||||
'selectedRoleTitles' => $roleContext['titles'],
|
||||
'inheritedPermissions' => $roleContext['permissions'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +192,13 @@ class UserManagement extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
if ($this->isPrivilegedUser($user) && ! $this->canManageAdmins()) {
|
||||
return view('Setting/Admin/UserManagement/index', [
|
||||
'users' => $userModel->withIdentities()->withGroups()->findAll(),
|
||||
'errors' => ['You cannot edit an administrator account.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$emailIdentity = $user->getEmailIdentity();
|
||||
$email = $emailIdentity ? (string) ($emailIdentity->secret ?? '') : '';
|
||||
$meta = $this->decodeIdentityExtra($emailIdentity->extra ?? null);
|
||||
|
|
@ -147,13 +208,15 @@ class UserManagement extends BaseController
|
|||
'email' => $email,
|
||||
'person_name' => trim((string) ($meta['full_name'] ?? '')),
|
||||
'phone_no' => trim((string) ($meta['phone_no'] ?? '')),
|
||||
'roles' => $this->availableRoles(),
|
||||
'current_role' => (string) (($user->getGroups() ?? [])[0] ?? ''),
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(int $userId)
|
||||
{
|
||||
$userModel = model(UserModel::class);
|
||||
$user = $userModel->find($userId);
|
||||
$user = $userModel->withGroups()->find($userId);
|
||||
if ($user === null) {
|
||||
$users = $userModel
|
||||
->withIdentities()
|
||||
|
|
@ -166,12 +229,26 @@ class UserManagement extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
if ($this->isPrivilegedUser($user) && ! $this->canManageAdmins()) {
|
||||
return view('Setting/Admin/UserManagement/index', [
|
||||
'users' => $userModel->withIdentities()->withGroups()->findAll(),
|
||||
'errors' => ['You cannot edit an administrator account.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$username = trim((string) $this->request->getPost('username'));
|
||||
$email = trim((string) $this->request->getPost('email'));
|
||||
$personName = trim((string) $this->request->getPost('person_name'));
|
||||
$phoneNo = trim((string) $this->request->getPost('phone_no'));
|
||||
$password = (string) $this->request->getPost('password');
|
||||
$active = $this->request->getPost('active') ? 1 : 0;
|
||||
$role = strtolower(trim((string) $this->request->getPost('role')));
|
||||
$oldGroups = $user->getGroups() ?? [];
|
||||
$oldState = [
|
||||
'username' => (string) ($user->username ?? ''),
|
||||
'groups' => $oldGroups,
|
||||
'active' => (int) ($user->active ?? 0),
|
||||
];
|
||||
|
||||
$errors = [];
|
||||
if ($username === '' || strlen($username) < 3 || strlen($username) > 30 || preg_match('/\A[a-zA-Z0-9\.]+\z/', $username) !== 1) {
|
||||
|
|
@ -189,6 +266,12 @@ class UserManagement extends BaseController
|
|||
if ($password !== '' && strlen($password) < 8) {
|
||||
$errors['password'] = 'Password must be at least 8 characters.';
|
||||
}
|
||||
if (! array_key_exists($role, $this->availableRoles())) {
|
||||
$errors['role'] = 'Invalid role selected.';
|
||||
}
|
||||
if ($userId === $this->currentUserId() && $active !== 1) {
|
||||
$errors['active'] = 'You cannot deactivate your own account.';
|
||||
}
|
||||
|
||||
$tables = config('Auth')->tables;
|
||||
$usersTable = (string) ($tables['users'] ?? 'users');
|
||||
|
|
@ -223,6 +306,8 @@ class UserManagement extends BaseController
|
|||
'email' => $email,
|
||||
'person_name' => $personName,
|
||||
'phone_no' => $phoneNo,
|
||||
'roles' => $this->availableRoles(),
|
||||
'current_role' => $role,
|
||||
'errors' => $errors,
|
||||
'formData' => [
|
||||
'username' => $username,
|
||||
|
|
@ -230,6 +315,7 @@ class UserManagement extends BaseController
|
|||
'person_name' => $personName,
|
||||
'phone_no' => $phoneNo,
|
||||
'active' => $active,
|
||||
'role' => $role,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
|
@ -242,7 +328,17 @@ class UserManagement extends BaseController
|
|||
}
|
||||
|
||||
$userModel->save($user);
|
||||
$user->syncGroups($role);
|
||||
$this->syncIdentityMeta($userId, $personName, $phoneNo);
|
||||
$this->auditClinicalUpdate('user_management', 'update', $userId, $oldState, [
|
||||
'username' => $username,
|
||||
'groups' => [$role],
|
||||
'active' => $active,
|
||||
]);
|
||||
|
||||
if ($active !== 1) {
|
||||
(new UserSessionRegistry($this->db))->revokeUser($userId, $this->currentUserId(), 'account_deactivated');
|
||||
}
|
||||
|
||||
$users = $userModel
|
||||
->withIdentities()
|
||||
|
|
@ -272,6 +368,13 @@ class UserManagement extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
if ($this->isPrivilegedUser($user) && ! $this->canManageAdmins()) {
|
||||
return view('Setting/Admin/UserManagement/index', [
|
||||
'users' => $userModel->withIdentities()->withGroups()->findAll(),
|
||||
'errors' => ['You cannot reset an administrator password.'],
|
||||
]);
|
||||
}
|
||||
|
||||
return view('Setting/Admin/UserManagement/reset_password', [
|
||||
'user' => $user,
|
||||
]);
|
||||
|
|
@ -280,7 +383,7 @@ class UserManagement extends BaseController
|
|||
public function resetPassword(int $userId)
|
||||
{
|
||||
$userModel = model(UserModel::class);
|
||||
$user = $userModel->find($userId);
|
||||
$user = $userModel->withGroups()->find($userId);
|
||||
if ($user === null) {
|
||||
$users = $userModel
|
||||
->withIdentities()
|
||||
|
|
@ -292,6 +395,12 @@ class UserManagement extends BaseController
|
|||
'errors' => ['User not found.'],
|
||||
]);
|
||||
}
|
||||
if ($this->isPrivilegedUser($user) && ! $this->canManageAdmins()) {
|
||||
return view('Setting/Admin/UserManagement/index', [
|
||||
'users' => $userModel->withIdentities()->withGroups()->findAll(),
|
||||
'errors' => ['You cannot reset an administrator password.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$password = trim((string) $this->request->getPost('password'));
|
||||
$passwordConfirm = trim((string) $this->request->getPost('password_confirm'));
|
||||
|
|
@ -352,6 +461,8 @@ class UserManagement extends BaseController
|
|||
'force_reset' => 1,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
(new UserSessionRegistry($this->db))->revokeUser($userId, $this->currentUserId(), 'password_reset');
|
||||
$this->auditClinicalUpdate('user_management', 'password_reset', $userId, null, ['force_reset' => 1]);
|
||||
|
||||
$users = $userModel
|
||||
->withIdentities()
|
||||
|
|
@ -473,7 +584,9 @@ class UserManagement extends BaseController
|
|||
return redirect()->back()->withInput()->with('errors', $errors);
|
||||
}
|
||||
|
||||
$oldPermissions = $userModel->withPermissions()->find($userId)?->getPermissions() ?? [];
|
||||
$user->syncPermissions(...$permissions);
|
||||
$this->auditClinicalUpdate('user_management', 'permissions', $userId, $oldPermissions, $permissions);
|
||||
|
||||
if ($this->request->isAJAX()) {
|
||||
$users = $userModel
|
||||
|
|
@ -482,11 +595,14 @@ class UserManagement extends BaseController
|
|||
->findAll();
|
||||
|
||||
$selectedUser = $userModel->withPermissions()->find($userId);
|
||||
$roleContext = $this->rolePermissionContext($selectedUser);
|
||||
|
||||
return view('Setting/Admin/UserManagement/permissions', [
|
||||
'users' => $users,
|
||||
'permissions' => setting('AuthGroups.permissions'),
|
||||
'selectedUser' => $selectedUser,
|
||||
'selectedRoleTitles' => $roleContext['titles'],
|
||||
'inheritedPermissions' => $roleContext['permissions'],
|
||||
'message' => 'Permissions updated successfully.',
|
||||
]);
|
||||
}
|
||||
|
|
@ -495,4 +611,117 @@ class UserManagement extends BaseController
|
|||
->to('setting/admin/user-management/permissions?user_id=' . $userId)
|
||||
->with('message', 'Permissions updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{titles: list<string>, permissions: list<string>}
|
||||
*/
|
||||
private function rolePermissionContext(?User $user): array
|
||||
{
|
||||
if ($user === null) {
|
||||
return ['titles' => [], 'permissions' => []];
|
||||
}
|
||||
|
||||
$availablePermissions = array_keys((array) setting('AuthGroups.permissions'));
|
||||
$groups = (array) setting('AuthGroups.groups');
|
||||
$matrix = (array) setting('AuthGroups.matrix');
|
||||
$inheritedPermissions = [];
|
||||
$roleTitles = [];
|
||||
|
||||
foreach ($user->getGroups() ?? [] as $role) {
|
||||
$roleTitles[] = (string) ($groups[$role]['title'] ?? $role);
|
||||
foreach ($matrix[$role] ?? [] as $grant) {
|
||||
if (str_ends_with($grant, '.*')) {
|
||||
$prefix = substr($grant, 0, -1);
|
||||
foreach ($availablePermissions as $permission) {
|
||||
if (str_starts_with($permission, $prefix)) {
|
||||
$inheritedPermissions[] = $permission;
|
||||
}
|
||||
}
|
||||
} elseif (in_array($grant, $availablePermissions, true)) {
|
||||
$inheritedPermissions[] = $grant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'titles' => array_values(array_unique($roleTitles)),
|
||||
'permissions' => array_values(array_unique($inheritedPermissions)),
|
||||
];
|
||||
}
|
||||
|
||||
public function sessions(): string
|
||||
{
|
||||
$registry = new UserSessionRegistry($this->db);
|
||||
$registry->cleanup();
|
||||
$rows = $registry->onlineUsers();
|
||||
foreach ($rows as &$row) {
|
||||
$meta = $this->decodeIdentityExtra($row['extra'] ?? null);
|
||||
$row['person_name'] = trim((string) ($meta['full_name'] ?? ''));
|
||||
unset($row['extra']);
|
||||
}
|
||||
unset($row);
|
||||
|
||||
return view('Setting/Admin/UserManagement/sessions', [
|
||||
'sessions' => $rows,
|
||||
'current_user_id' => $this->currentUserId(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function forceLogout(int $userId)
|
||||
{
|
||||
if ($userId === $this->currentUserId()) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Use Sign Out to end your own session.']);
|
||||
}
|
||||
|
||||
$user = model(UserModel::class)->withGroups()->find($userId);
|
||||
if ($user === null) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'User not found.']);
|
||||
}
|
||||
if ($this->isPrivilegedUser($user) && ! $this->canManageAdmins()) {
|
||||
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'You cannot log out an administrator.']);
|
||||
}
|
||||
|
||||
$count = (new UserSessionRegistry($this->db))->revokeUser($userId, $this->currentUserId());
|
||||
$this->auditClinicalUpdate('user_management', 'force_logout', $userId, null, ['sessions_revoked' => $count]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'update' => 1,
|
||||
'error_text' => $count > 0 ? 'User session ended.' : 'No active session found.',
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(int $userId)
|
||||
{
|
||||
if ($userId === $this->currentUserId()) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'You cannot delete your own account.']);
|
||||
}
|
||||
|
||||
$userModel = model(UserModel::class);
|
||||
$user = $userModel->withGroups()->find($userId);
|
||||
if ($user === null) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'User not found.']);
|
||||
}
|
||||
if ($this->isPrivilegedUser($user) && ! $this->canManageAdmins()) {
|
||||
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'You cannot delete an administrator.']);
|
||||
}
|
||||
|
||||
(new UserSessionRegistry($this->db))->revokeUser($userId, $this->currentUserId(), 'account_deleted');
|
||||
$this->auditClinicalUpdate('user_management', 'delete', $userId, [
|
||||
'username' => $user->username,
|
||||
'groups' => $user->getGroups() ?? [],
|
||||
], ['deleted' => true]);
|
||||
|
||||
if (! $userModel->delete($userId)) {
|
||||
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to delete user.']);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'update' => 1,
|
||||
'error_text' => 'User deleted successfully.',
|
||||
'csrfName' => csrf_token(),
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class CreateUserActiveSessionsTable extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'BIGINT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'user_id' => ['type' => 'INT', 'unsigned' => true],
|
||||
'session_token' => ['type' => 'CHAR', 'constraint' => 64],
|
||||
'session_id_hash' => ['type' => 'CHAR', 'constraint' => 64],
|
||||
'ip_address' => ['type' => 'VARCHAR', 'constraint' => 45, 'null' => true],
|
||||
'user_agent' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'login_at' => ['type' => 'DATETIME'],
|
||||
'last_activity' => ['type' => 'DATETIME'],
|
||||
'logout_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'is_active' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 1],
|
||||
'revoked_reason' => ['type' => 'VARCHAR', 'constraint' => 60, 'null' => true],
|
||||
'revoked_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
]);
|
||||
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addUniqueKey('session_token', 'uk_user_active_session_token');
|
||||
$this->forge->addKey(['user_id', 'is_active'], false, false, 'idx_user_active_sessions_user');
|
||||
$this->forge->addKey(['is_active', 'last_activity'], false, false, 'idx_user_active_sessions_online');
|
||||
$this->forge->createTable('user_active_sessions', true);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('user_active_sessions', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class UseInnoDbForUserActiveSessions extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$table = $this->db->query("SHOW TABLES LIKE 'user_active_sessions'")->getRowArray();
|
||||
if ($table !== null && $this->db->DBDriver === 'MySQLi') {
|
||||
$this->db->query('ALTER TABLE user_active_sessions ENGINE=InnoDB');
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
use Config\AuthGroups;
|
||||
|
||||
class CreateAuthRoleMasterTables extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'BIGINT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'alias' => ['type' => 'VARCHAR', 'constraint' => 64],
|
||||
'title' => ['type' => 'VARCHAR', 'constraint' => 120],
|
||||
'description' => ['type' => 'VARCHAR', 'constraint' => 255, 'null' => true],
|
||||
'is_active' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 1],
|
||||
'is_builtin' => ['type' => 'TINYINT', 'constraint' => 1, 'default' => 0],
|
||||
'created_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'updated_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
'updated_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addUniqueKey('alias', 'uk_auth_roles_alias');
|
||||
$this->forge->addKey(['is_active', 'title'], false, false, 'idx_auth_roles_active_title');
|
||||
$this->forge->createTable('auth_roles', true, ['ENGINE' => 'InnoDB']);
|
||||
|
||||
$this->forge->addField([
|
||||
'id' => ['type' => 'BIGINT', 'unsigned' => true, 'auto_increment' => true],
|
||||
'role_id' => ['type' => 'BIGINT', 'unsigned' => true],
|
||||
'permission' => ['type' => 'VARCHAR', 'constraint' => 191],
|
||||
'created_by' => ['type' => 'INT', 'unsigned' => true, 'null' => true],
|
||||
'created_at' => ['type' => 'DATETIME', 'null' => true],
|
||||
]);
|
||||
$this->forge->addPrimaryKey('id');
|
||||
$this->forge->addUniqueKey(['role_id', 'permission'], 'uk_auth_role_permission');
|
||||
$this->forge->addKey('permission', false, false, 'idx_auth_role_permissions_permission');
|
||||
$this->forge->addForeignKey('role_id', 'auth_roles', 'id', 'CASCADE', 'CASCADE', 'fk_auth_role_permissions_role');
|
||||
$this->forge->createTable('auth_role_permissions', true, ['ENGINE' => 'InnoDB']);
|
||||
|
||||
$config = new AuthGroups();
|
||||
$now = date('Y-m-d H:i:s');
|
||||
foreach ($config->groups as $alias => $role) {
|
||||
$this->db->table('auth_roles')->insert([
|
||||
'alias' => $alias,
|
||||
'title' => $role['title'] ?? $alias,
|
||||
'description' => $role['description'] ?? null,
|
||||
'is_active' => 1,
|
||||
'is_builtin' => 1,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$roleId = (int) ($this->db->table('auth_roles')
|
||||
->select('id')
|
||||
->where('alias', $alias)
|
||||
->get(1)
|
||||
->getRowArray()['id'] ?? 0);
|
||||
foreach ($config->matrix[$alias] ?? [] as $permission) {
|
||||
$this->db->table('auth_role_permissions')->insert([
|
||||
'role_id' => $roleId,
|
||||
'permission' => $permission,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
$this->forge->dropTable('auth_role_permissions', true);
|
||||
$this->forge->dropTable('auth_roles', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use App\Libraries\RoleRegistry;
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddDevelopedModulePermissions extends Migration
|
||||
{
|
||||
private const NEW_PERMISSIONS = [
|
||||
'reports.billing_operations.view',
|
||||
'reports.document_issue.view',
|
||||
'finance.access',
|
||||
'finance.compliance.view',
|
||||
'finance.doctor_payout.manage',
|
||||
'finance.vendor.manage',
|
||||
'finance.po.manage',
|
||||
'finance.grn.manage',
|
||||
'finance.invoice.manage',
|
||||
'doctor_work.immunization.access',
|
||||
'doctor_work.immunization.schedule-manage',
|
||||
'doctor_work.immunization.record-manage',
|
||||
'abdm.abha.create',
|
||||
'billing.payment_request.view',
|
||||
'billing.payment_request.manage',
|
||||
'billing.refund.view',
|
||||
'billing.refund.manage',
|
||||
'billing.items.view',
|
||||
'billing.items.manage',
|
||||
'billing.packages.view',
|
||||
'billing.packages.manage',
|
||||
];
|
||||
|
||||
public function up()
|
||||
{
|
||||
if (! $this->tableExists('auth_roles') || ! $this->tableExists('auth_role_permissions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$doctor = $this->db->table('auth_roles')->where('alias', 'doctor')->get(1)->getRowArray();
|
||||
if ($doctor === null) {
|
||||
$this->db->table('auth_roles')->insert([
|
||||
'alias' => 'doctor',
|
||||
'title' => 'Doctor',
|
||||
'description' => 'Clinical doctor access to OPD work, diagnosis reports, and immunization records.',
|
||||
'is_active' => 1,
|
||||
'is_builtin' => 1,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$doctor = $this->db->table('auth_roles')->where('alias', 'doctor')->get(1)->getRowArray();
|
||||
}
|
||||
|
||||
$roleGrants = [
|
||||
'doctor' => [
|
||||
'opd.doctor-panel.access',
|
||||
'doctor_work.access',
|
||||
'doctor_work.immunization.access',
|
||||
'doctor_work.immunization.record-manage',
|
||||
'diagnosis.access',
|
||||
'diagnosis.report.view',
|
||||
],
|
||||
'superadmin' => ['reports.billing_operations.view', 'reports.document_issue.view'],
|
||||
'admin' => ['reports.billing_operations.view', 'reports.document_issue.view'],
|
||||
'developer' => ['reports.billing_operations.view', 'reports.document_issue.view'],
|
||||
'billing_cashier' => [
|
||||
'abdm.abha.create',
|
||||
'billing.payment_request.view',
|
||||
'billing.payment_request.manage',
|
||||
'billing.refund.view',
|
||||
'billing.refund.manage',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($roleGrants as $alias => $permissions) {
|
||||
$role = $this->db->table('auth_roles')->select('id')->where('alias', $alias)->get(1)->getRowArray();
|
||||
if ($role === null) {
|
||||
continue;
|
||||
}
|
||||
foreach ($permissions as $permission) {
|
||||
$this->insertRolePermission((int) $role['id'], $permission, $now);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->tableExists('auth_permissions_users')) {
|
||||
$managerIds = array_column(
|
||||
$this->db->table('auth_permissions_users')->select('user_id')->where('permission', 'users.manage-admins')->get()->getResultArray(),
|
||||
'user_id'
|
||||
);
|
||||
foreach ($managerIds as $userId) {
|
||||
foreach (self::NEW_PERMISSIONS as $permission) {
|
||||
$exists = $this->db->table('auth_permissions_users')->where('user_id', (int) $userId)->where('permission', $permission)->countAllResults();
|
||||
if ($exists === 0) {
|
||||
$this->db->table('auth_permissions_users')->insert([
|
||||
'user_id' => (int) $userId,
|
||||
'permission' => $permission,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(new RoleRegistry($this->db))->publish();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->tableExists('auth_permissions_users')) {
|
||||
$this->db->table('auth_permissions_users')->whereIn('permission', self::NEW_PERMISSIONS)->delete();
|
||||
}
|
||||
if ($this->tableExists('auth_role_permissions')) {
|
||||
$this->db->table('auth_role_permissions')->whereIn('permission', self::NEW_PERMISSIONS)->delete();
|
||||
}
|
||||
if ($this->tableExists('auth_roles')) {
|
||||
$this->db->table('auth_roles')->where('alias', 'doctor')->where('is_builtin', 1)->delete();
|
||||
(new RoleRegistry($this->db))->publish();
|
||||
}
|
||||
}
|
||||
|
||||
private function insertRolePermission(int $roleId, string $permission, string $createdAt): void
|
||||
{
|
||||
$exists = $this->db->table('auth_role_permissions')->where('role_id', $roleId)->where('permission', $permission)->countAllResults();
|
||||
if ($exists === 0) {
|
||||
$this->db->table('auth_role_permissions')->insert([
|
||||
'role_id' => $roleId,
|
||||
'permission' => $permission,
|
||||
'created_at' => $createdAt,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function tableExists(string $table): bool
|
||||
{
|
||||
return $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray() !== null;
|
||||
}
|
||||
}
|
||||
50
app/Filters/SingleActiveSessionFilter.php
Normal file
50
app/Filters/SingleActiveSessionFilter.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Filters;
|
||||
|
||||
use App\Libraries\UserSessionRegistry;
|
||||
use CodeIgniter\Filters\FilterInterface;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
class SingleActiveSessionFilter implements FilterInterface
|
||||
{
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
if (! function_exists('auth') || ! auth()->loggedIn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$userId = (int) (auth()->user()->id ?? 0);
|
||||
if ($userId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$registry = new UserSessionRegistry();
|
||||
$status = $registry->ensureCurrent($userId);
|
||||
if (($status['active'] ?? false) === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$message = ($status['reason'] ?? '') === 'signed_in_elsewhere'
|
||||
? 'Your account was signed in on another system. This session has been logged out.'
|
||||
: 'Your login session is no longer active. Please sign in again.';
|
||||
|
||||
auth()->logout();
|
||||
|
||||
$isAjax = strtolower($request->getHeaderLine('X-Requested-With')) === 'xmlhttprequest';
|
||||
if ($isAjax || str_contains(strtolower($request->getHeaderLine('Accept')), 'application/json')) {
|
||||
return service('response')->setStatusCode(409)->setJSON([
|
||||
'session_terminated' => true,
|
||||
'message' => $message,
|
||||
'login_url' => base_url('login'),
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->to(base_url('login'))->with('error', $message);
|
||||
}
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): void
|
||||
{
|
||||
}
|
||||
}
|
||||
219
app/Libraries/RoleRegistry.php
Normal file
219
app/Libraries/RoleRegistry.php
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
class RoleRegistry
|
||||
{
|
||||
private BaseConnection $db;
|
||||
|
||||
public function __construct(?BaseConnection $db = null)
|
||||
{
|
||||
$this->db = $db ?? db_connect();
|
||||
}
|
||||
|
||||
public function all(): array
|
||||
{
|
||||
$roles = $this->db->table('auth_roles r')
|
||||
->select('r.*, COUNT(DISTINCT rp.id) AS permission_count, COUNT(DISTINCT gu.id) AS user_count')
|
||||
->join('auth_role_permissions rp', 'rp.role_id = r.id', 'left')
|
||||
->join('auth_groups_users gu', 'gu.group = r.alias', 'left')
|
||||
->groupBy('r.id')
|
||||
->orderBy('r.is_builtin', 'DESC')
|
||||
->orderBy('r.title', 'ASC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
public function find(int $roleId): ?array
|
||||
{
|
||||
$role = $this->db->table('auth_roles')->where('id', $roleId)->get(1)->getRowArray();
|
||||
if ($role === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$role['permissions'] = array_column(
|
||||
$this->db->table('auth_role_permissions')->select('permission')->where('role_id', $roleId)->orderBy('permission')->get()->getResultArray(),
|
||||
'permission'
|
||||
);
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
public function activeGroups(): array
|
||||
{
|
||||
$groups = [];
|
||||
foreach ($this->db->table('auth_roles')->where('is_active', 1)->orderBy('title')->get()->getResultArray() as $role) {
|
||||
$groups[$role['alias']] = [
|
||||
'title' => $role['title'],
|
||||
'description' => $role['description'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function save(?int $roleId, array $input, int $userId): int
|
||||
{
|
||||
$alias = strtolower(trim((string) ($input['alias'] ?? '')));
|
||||
$title = trim((string) ($input['title'] ?? ''));
|
||||
$description = trim((string) ($input['description'] ?? ''));
|
||||
$permissions = $this->validatePermissions((array) ($input['permissions'] ?? []));
|
||||
|
||||
if (! preg_match('/^[a-z][a-z0-9_]{2,63}$/', $alias)) {
|
||||
throw new InvalidArgumentException('Role code must start with a letter and contain only lowercase letters, numbers, and underscores.');
|
||||
}
|
||||
if ($title === '' || mb_strlen($title) > 120) {
|
||||
throw new InvalidArgumentException('Role name is required and must be at most 120 characters.');
|
||||
}
|
||||
if (mb_strlen($description) > 255) {
|
||||
throw new InvalidArgumentException('Description must be at most 255 characters.');
|
||||
}
|
||||
|
||||
$existing = $this->db->table('auth_roles')->where('alias', $alias)->get(1)->getRowArray();
|
||||
if ($existing !== null && (int) $existing['id'] !== (int) $roleId) {
|
||||
throw new InvalidArgumentException('Role code is already in use.');
|
||||
}
|
||||
|
||||
$current = $roleId ? $this->find($roleId) : null;
|
||||
if ($roleId && $current === null) {
|
||||
throw new InvalidArgumentException('Role not found.');
|
||||
}
|
||||
if ($current !== null && (int) $current['is_builtin'] === 1 && $alias !== $current['alias']) {
|
||||
throw new InvalidArgumentException('Built-in role codes cannot be changed.');
|
||||
}
|
||||
if (($current['alias'] ?? '') === 'superadmin') {
|
||||
$permissions = $current['permissions'];
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$this->db->transStart();
|
||||
if ($current === null) {
|
||||
$this->db->table('auth_roles')->insert([
|
||||
'alias' => $alias,
|
||||
'title' => $title,
|
||||
'description' => $description !== '' ? $description : null,
|
||||
'is_active' => 1,
|
||||
'is_builtin' => 0,
|
||||
'created_by' => $userId ?: null,
|
||||
'updated_by' => $userId ?: null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$roleId = (int) $this->db->insertID();
|
||||
} else {
|
||||
$this->db->table('auth_roles')->where('id', $roleId)->update([
|
||||
'title' => $title,
|
||||
'description' => $description !== '' ? $description : null,
|
||||
'updated_by' => $userId ?: null,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
$this->db->table('auth_role_permissions')->where('role_id', $roleId)->delete();
|
||||
}
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
$this->db->table('auth_role_permissions')->insert([
|
||||
'role_id' => $roleId,
|
||||
'permission' => $permission,
|
||||
'created_by' => $userId ?: null,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
}
|
||||
$this->db->transComplete();
|
||||
if (! $this->db->transStatus()) {
|
||||
throw new RuntimeException('Unable to save role.');
|
||||
}
|
||||
|
||||
$this->publish();
|
||||
return (int) $roleId;
|
||||
}
|
||||
|
||||
public function setActive(int $roleId, bool $active, int $userId): void
|
||||
{
|
||||
$role = $this->find($roleId);
|
||||
if ($role === null) {
|
||||
throw new InvalidArgumentException('Role not found.');
|
||||
}
|
||||
if ((int) $role['is_builtin'] === 1) {
|
||||
throw new InvalidArgumentException('Built-in roles cannot be deactivated.');
|
||||
}
|
||||
|
||||
$this->db->table('auth_roles')->where('id', $roleId)->update([
|
||||
'is_active' => $active ? 1 : 0,
|
||||
'updated_by' => $userId ?: null,
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$this->publish();
|
||||
}
|
||||
|
||||
public function delete(int $roleId): void
|
||||
{
|
||||
$role = $this->find($roleId);
|
||||
if ($role === null) {
|
||||
throw new InvalidArgumentException('Role not found.');
|
||||
}
|
||||
if ((int) $role['is_builtin'] === 1) {
|
||||
throw new InvalidArgumentException('Built-in roles cannot be deleted.');
|
||||
}
|
||||
if ((int) $role['is_active'] === 1) {
|
||||
throw new InvalidArgumentException('Deactivate the role before deleting it.');
|
||||
}
|
||||
$assignments = $this->db->table('auth_groups_users')->where('group', $role['alias'])->countAllResults();
|
||||
if ($assignments > 0) {
|
||||
throw new InvalidArgumentException('This role is assigned to users and cannot be deleted.');
|
||||
}
|
||||
|
||||
$this->db->table('auth_roles')->where('id', $roleId)->delete();
|
||||
$this->publish();
|
||||
}
|
||||
|
||||
public function publish(): void
|
||||
{
|
||||
$groups = $this->activeGroups();
|
||||
$matrix = [];
|
||||
$rows = $this->db->table('auth_roles r')
|
||||
->select('r.alias,rp.permission')
|
||||
->join('auth_role_permissions rp', 'rp.role_id = r.id', 'left')
|
||||
->where('r.is_active', 1)
|
||||
->orderBy('r.id')
|
||||
->get()
|
||||
->getResultArray();
|
||||
foreach ($groups as $alias => $_role) {
|
||||
$matrix[$alias] = [];
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
if (! empty($row['permission'])) {
|
||||
$matrix[$row['alias']][] = $row['permission'];
|
||||
}
|
||||
}
|
||||
|
||||
setting('AuthGroups.groups', $groups);
|
||||
setting('AuthGroups.matrix', $matrix);
|
||||
}
|
||||
|
||||
private function validatePermissions(array $permissions): array
|
||||
{
|
||||
$permissions = array_values(array_unique(array_filter(array_map(static fn ($value): string => strtolower(trim((string) $value)), $permissions))));
|
||||
$known = array_keys((array) setting('AuthGroups.permissions'));
|
||||
$allowedWildcards = [];
|
||||
foreach ($known as $permission) {
|
||||
$scope = strstr($permission, '.', true);
|
||||
if ($scope !== false) {
|
||||
$allowedWildcards[] = $scope . '.*';
|
||||
}
|
||||
}
|
||||
$allowed = array_merge($known, array_unique($allowedWildcards));
|
||||
foreach ($permissions as $permission) {
|
||||
if (! in_array($permission, $allowed, true)) {
|
||||
throw new InvalidArgumentException('Unknown or unsupported permission: ' . $permission);
|
||||
}
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
}
|
||||
183
app/Libraries/UserSessionRegistry.php
Normal file
183
app/Libraries/UserSessionRegistry.php
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
|
||||
class UserSessionRegistry
|
||||
{
|
||||
public const SESSION_TOKEN_KEY = 'hms_active_session_token';
|
||||
private const TOUCH_KEY = 'hms_active_session_touched_at';
|
||||
private const ONLINE_SECONDS = 120;
|
||||
|
||||
private BaseConnection $db;
|
||||
|
||||
public function __construct(?BaseConnection $db = null)
|
||||
{
|
||||
$this->db = $db ?? db_connect();
|
||||
}
|
||||
|
||||
public function activate(int $userId): string
|
||||
{
|
||||
$this->ensureTable();
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$now = date('Y-m-d H:i:s');
|
||||
|
||||
$this->db->transStart();
|
||||
$this->db->table('user_active_sessions')
|
||||
->where('user_id', $userId)
|
||||
->where('is_active', 1)
|
||||
->update([
|
||||
'is_active' => 0,
|
||||
'logout_at' => $now,
|
||||
'revoked_reason' => 'signed_in_elsewhere',
|
||||
]);
|
||||
|
||||
$request = service('request');
|
||||
$this->db->table('user_active_sessions')->insert([
|
||||
'user_id' => $userId,
|
||||
'session_token' => $token,
|
||||
'session_id_hash' => hash('sha256', session_id()),
|
||||
'ip_address' => $request instanceof IncomingRequest ? $request->getIPAddress() : null,
|
||||
'user_agent' => $request instanceof IncomingRequest ? substr($request->getUserAgent()->getAgentString(), 0, 255) : null,
|
||||
'login_at' => $now,
|
||||
'last_activity' => $now,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
$this->db->transComplete();
|
||||
|
||||
session()->set(self::SESSION_TOKEN_KEY, $token);
|
||||
session()->set(self::TOUCH_KEY, time());
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function ensureCurrent(int $userId): array
|
||||
{
|
||||
$this->ensureTable();
|
||||
$token = trim((string) session()->get(self::SESSION_TOKEN_KEY));
|
||||
if ($token === '') {
|
||||
$this->activate($userId);
|
||||
return ['active' => true, 'reason' => null];
|
||||
}
|
||||
|
||||
$row = $this->db->table('user_active_sessions')
|
||||
->select('is_active,revoked_reason')
|
||||
->where('user_id', $userId)
|
||||
->where('session_token', $token)
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
|
||||
if (empty($row) || (int) ($row['is_active'] ?? 0) !== 1) {
|
||||
return [
|
||||
'active' => false,
|
||||
'reason' => (string) ($row['revoked_reason'] ?? 'session_expired'),
|
||||
];
|
||||
}
|
||||
|
||||
$lastTouch = (int) session()->get(self::TOUCH_KEY);
|
||||
if ($lastTouch <= time() - 30) {
|
||||
$this->db->table('user_active_sessions')
|
||||
->where('user_id', $userId)
|
||||
->where('session_token', $token)
|
||||
->where('is_active', 1)
|
||||
->update(['last_activity' => date('Y-m-d H:i:s')]);
|
||||
session()->set(self::TOUCH_KEY, time());
|
||||
}
|
||||
|
||||
return ['active' => true, 'reason' => null];
|
||||
}
|
||||
|
||||
public function deactivateCurrent(string $reason = 'logout'): void
|
||||
{
|
||||
if (! $this->db->tableExists('user_active_sessions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$token = trim((string) session()->get(self::SESSION_TOKEN_KEY));
|
||||
if ($token !== '') {
|
||||
$this->db->table('user_active_sessions')
|
||||
->where('session_token', $token)
|
||||
->where('is_active', 1)
|
||||
->update([
|
||||
'is_active' => 0,
|
||||
'logout_at' => date('Y-m-d H:i:s'),
|
||||
'revoked_reason' => $reason,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function revokeUser(int $userId, int $revokedBy, string $reason = 'admin_force_logout'): int
|
||||
{
|
||||
$this->ensureTable();
|
||||
$this->db->table('user_active_sessions')
|
||||
->where('user_id', $userId)
|
||||
->where('is_active', 1)
|
||||
->update([
|
||||
'is_active' => 0,
|
||||
'logout_at' => date('Y-m-d H:i:s'),
|
||||
'revoked_reason' => $reason,
|
||||
'revoked_by' => $revokedBy,
|
||||
]);
|
||||
|
||||
return $this->db->affectedRows();
|
||||
}
|
||||
|
||||
public function onlineUsers(): array
|
||||
{
|
||||
$this->ensureTable();
|
||||
$cutoff = date('Y-m-d H:i:s', time() - self::ONLINE_SECONDS);
|
||||
|
||||
return $this->db->table('user_active_sessions s')
|
||||
->select('s.id,s.user_id,s.ip_address,s.user_agent,s.login_at,s.last_activity,u.username,i.extra')
|
||||
->join('users u', 'u.id = s.user_id', 'inner')
|
||||
->join('auth_identities i', "i.user_id = u.id AND i.type = 'email_password'", 'left')
|
||||
->where('s.is_active', 1)
|
||||
->where('s.last_activity >=', $cutoff)
|
||||
->orderBy('s.last_activity', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
public function cleanup(): void
|
||||
{
|
||||
if (! $this->db->tableExists('user_active_sessions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->table('user_active_sessions')
|
||||
->where('is_active', 1)
|
||||
->where('last_activity <', date('Y-m-d H:i:s', time() - 7200))
|
||||
->update([
|
||||
'is_active' => 0,
|
||||
'logout_at' => date('Y-m-d H:i:s'),
|
||||
'revoked_reason' => 'expired',
|
||||
]);
|
||||
}
|
||||
|
||||
private function ensureTable(): void
|
||||
{
|
||||
if ($this->db->tableExists('user_active_sessions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->query("CREATE TABLE IF NOT EXISTS user_active_sessions (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
session_token CHAR(64) NOT NULL,
|
||||
session_id_hash CHAR(64) NOT NULL,
|
||||
ip_address VARCHAR(45) NULL,
|
||||
user_agent VARCHAR(255) NULL,
|
||||
login_at DATETIME NOT NULL,
|
||||
last_activity DATETIME NOT NULL,
|
||||
logout_at DATETIME NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
revoked_reason VARCHAR(60) NULL,
|
||||
revoked_by INT UNSIGNED NULL,
|
||||
UNIQUE KEY uk_user_active_session_token (session_token),
|
||||
KEY idx_user_active_sessions_user (user_id,is_active),
|
||||
KEY idx_user_active_sessions_online (is_active,last_activity)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
|
||||
}
|
||||
}
|
||||
83
app/Views/Setting/Admin/RoleManagement/form.php
Normal file
83
app/Views/Setting/Admin/RoleManagement/form.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
$role = $role ?? [];
|
||||
$isEdit = ! empty($role['id']);
|
||||
$isSuperAdmin = ($role['alias'] ?? '') === 'superadmin';
|
||||
$selectedPermissions = array_values((array) ($role['permissions'] ?? []));
|
||||
$allPermissions = array_merge($wildcards ?? [], $permissions ?? []);
|
||||
$grouped = [];
|
||||
foreach ($allPermissions as $key => $label) {
|
||||
$scope = strstr($key, '.', true) ?: 'other';
|
||||
$grouped[$scope][$key] = $label;
|
||||
}
|
||||
?>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div><h3 class="card-title mb-0"><?= $isEdit ? 'Edit Role' : 'New Role' ?></h3><div class="text-muted small">Users assigned this role inherit every selected permission.</div></div>
|
||||
<button class="btn btn-light" type="button" onclick="load_form_div('<?= base_url('setting/admin/roles') ?>','maindiv','Role Master');"><i class="bi bi-arrow-left"></i> Back to Role Master</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (! empty($errors)) : ?><div class="alert alert-danger"><?php foreach ($errors as $error) : ?><div><?= esc($error) ?></div><?php endforeach ?></div><?php endif ?>
|
||||
<?php if ($isSuperAdmin) : ?><div class="alert alert-info">Super Admin permissions are protected against lockout and cannot be changed.</div><?php endif ?>
|
||||
|
||||
<form id="frm_role_master" action="<?= $isEdit ? base_url('setting/admin/roles/edit/' . (int) $role['id']) : base_url('setting/admin/roles') ?>" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="alias">Role Code</label>
|
||||
<input class="form-control" id="alias" name="alias" type="text" maxlength="64" value="<?= esc($role['alias'] ?? '') ?>" placeholder="e.g. lab_manager" <?= $isEdit ? 'readonly' : 'required' ?>>
|
||||
<div class="form-text">Permanent lowercase system code; it cannot be changed later.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="title">Role Name</label>
|
||||
<input class="form-control" id="title" name="title" type="text" maxlength="120" value="<?= esc($role['title'] ?? '') ?>" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="description">Description</label>
|
||||
<input class="form-control" id="description" name="description" type="text" maxlength="255" value="<?= esc($role['description'] ?? '') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mt-4 mb-2">
|
||||
<div><strong>Inherited Permissions</strong><div class="text-muted small">Wildcard selections such as Pharmacy: All include current and future permissions in that module.</div></div>
|
||||
<?php if (! $isSuperAdmin) : ?><button class="btn btn-sm btn-outline-secondary" type="button" id="roleClearPermissions">Clear All</button><?php endif ?>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($grouped as $scope => $items) : ?>
|
||||
<div class="col-12 col-md-6 col-xl-4">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<div class="fw-bold mb-2"><?= esc(ucwords(str_replace('_', ' ', $scope))) ?></div>
|
||||
<?php foreach ($items as $key => $label) : ?>
|
||||
<div class="form-check mb-1">
|
||||
<input class="form-check-input role-permission" type="checkbox" name="permissions[]" id="role_perm_<?= esc(md5($key)) ?>" value="<?= esc($key) ?>" <?= in_array($key, $selectedPermissions, true) ? 'checked' : '' ?> <?= $isSuperAdmin ? 'disabled' : '' ?>>
|
||||
<label class="form-check-label" for="role_perm_<?= esc(md5($key)) ?>"><?= esc($label) ?> <code><?= esc($key) ?></code></label>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex gap-2">
|
||||
<button class="btn btn-primary" type="submit">Save Role</button>
|
||||
<button class="btn btn-light" type="button" onclick="load_form_div('<?= base_url('setting/admin/roles') ?>','maindiv','Role Master');">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
$(document).off('click.roleClear', '#roleClearPermissions').on('click.roleClear', '#roleClearPermissions', function() {
|
||||
$('.role-permission').prop('checked', false);
|
||||
});
|
||||
$(document).off('submit.roleMaster', '#frm_role_master').on('submit.roleMaster', '#frm_role_master', function(event) {
|
||||
event.preventDefault();
|
||||
var form = this;
|
||||
$.post($(form).attr('action'), $(form).serialize()).done(function(html) {
|
||||
$('#maindiv').html(html);
|
||||
}).fail(function(xhr) {
|
||||
alert((xhr.responseJSON && xhr.responseJSON.error_text) || 'Unable to save role.');
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
79
app/Views/Setting/Admin/RoleManagement/index.php
Normal file
79
app/Views/Setting/Admin/RoleManagement/index.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div>
|
||||
<h3 class="card-title mb-0">Role Master</h3>
|
||||
<div class="text-muted small">Define job roles and their inherited permissions.</div>
|
||||
</div>
|
||||
<div class="card-tools ms-auto d-flex gap-2">
|
||||
<button class="btn btn-primary" type="button" onclick="load_form_div('<?= base_url('setting/admin/roles/new') ?>','maindiv','New Role');">
|
||||
<i class="bi bi-plus-lg"></i> New Role
|
||||
</button>
|
||||
<button class="btn btn-light" type="button" onclick="load_form_div('<?= base_url('setting/admin/user-management') ?>','maindiv','User Management');">
|
||||
<i class="bi bi-arrow-left"></i> Back to Users
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (! empty($message)) : ?><div class="alert alert-success"><?= esc($message) ?></div><?php endif ?>
|
||||
<?php if (! empty($errors)) : ?>
|
||||
<div class="alert alert-danger"><?php foreach ($errors as $error) : ?><div><?= esc($error) ?></div><?php endforeach ?></div>
|
||||
<?php endif ?>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped align-middle">
|
||||
<thead><tr><th>Role</th><th>Code</th><th>Permissions</th><th>Users</th><th>Status</th><th>Type</th><th>Action</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach (($roles ?? []) as $role) : ?>
|
||||
<tr>
|
||||
<td><strong><?= esc($role['title']) ?></strong><div class="text-muted small"><?= esc($role['description'] ?? '') ?></div></td>
|
||||
<td><code><?= esc($role['alias']) ?></code></td>
|
||||
<td><?= (int) $role['permission_count'] ?></td>
|
||||
<td><?= (int) $role['user_count'] ?></td>
|
||||
<td><span class="badge <?= (int) $role['is_active'] === 1 ? 'bg-success' : 'bg-secondary' ?>"><?= (int) $role['is_active'] === 1 ? 'Active' : 'Inactive' ?></span></td>
|
||||
<td><?= (int) $role['is_builtin'] === 1 ? '<span class="badge bg-light text-dark">Built-in</span>' : 'Custom' ?></td>
|
||||
<td>
|
||||
<div class="d-flex gap-1 flex-wrap">
|
||||
<button class="btn btn-sm btn-outline-primary" type="button" onclick="load_form_div('<?= base_url('setting/admin/roles/edit/' . (int) $role['id']) ?>','maindiv','Edit Role');">Edit</button>
|
||||
<?php if ((int) $role['is_builtin'] !== 1) : ?>
|
||||
<button class="btn btn-sm btn-outline-secondary btn-role-status" type="button" data-role-id="<?= (int) $role['id'] ?>" data-active="<?= (int) $role['is_active'] === 1 ? 0 : 1 ?>"><?= (int) $role['is_active'] === 1 ? 'Deactivate' : 'Activate' ?></button>
|
||||
<?php if ((int) $role['is_active'] !== 1 && (int) $role['user_count'] === 0) : ?>
|
||||
<button class="btn btn-sm btn-outline-danger btn-role-delete" type="button" data-role-id="<?= (int) $role['id'] ?>" data-title="<?= esc($role['title'], 'attr') ?>">Delete</button>
|
||||
<?php endif ?>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var csrfName = '<?= csrf_token() ?>';
|
||||
var csrfHash = '<?= csrf_hash() ?>';
|
||||
|
||||
function postRole(url, data) {
|
||||
data[csrfName] = csrfHash;
|
||||
$.post(url, data, null, 'json').done(function(response) {
|
||||
csrfHash = response.csrf_hash || csrfHash;
|
||||
load_form_div('<?= base_url('setting/admin/roles') ?>', 'maindiv', 'Role Master');
|
||||
}).fail(function(xhr) {
|
||||
var response = xhr.responseJSON || {};
|
||||
csrfHash = response.csrf_hash || csrfHash;
|
||||
alert(response.error_text || 'Unable to update role.');
|
||||
});
|
||||
}
|
||||
|
||||
$(document).off('click.roleStatus', '.btn-role-status').on('click.roleStatus', '.btn-role-status', function() {
|
||||
postRole('<?= base_url('setting/admin/roles/status') ?>/' + $(this).data('role-id'), {is_active: $(this).data('active')});
|
||||
});
|
||||
|
||||
$(document).off('click.roleDelete', '.btn-role-delete').on('click.roleDelete', '.btn-role-delete', function() {
|
||||
if (!confirm('Delete role ' + $(this).data('title') + '?')) return;
|
||||
postRole('<?= base_url('setting/admin/roles/delete') ?>/' + $(this).data('role-id'), {});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -33,6 +33,14 @@
|
|||
<input class="form-control" id="email" name="email" type="email" value="<?= esc($formData['email'] ?? old('email')) ?>" required>
|
||||
<div class="invalid-feedback">Email is required.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="person_name">Person Name</label>
|
||||
<input class="form-control" id="person_name" name="person_name" type="text" maxlength="120" value="<?= esc($formData['person_name'] ?? old('person_name')) ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="phone_no">Phone No.</label>
|
||||
<input class="form-control" id="phone_no" name="phone_no" type="text" maxlength="20" value="<?= esc($formData['phone_no'] ?? old('phone_no')) ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="password">Password</label>
|
||||
<input class="form-control" id="password" name="password" type="password" required>
|
||||
|
|
@ -51,6 +59,7 @@
|
|||
<?php endif ?>
|
||||
</select>
|
||||
<div class="invalid-feedback">Role is required.</div>
|
||||
<div class="form-text">Choose the closest job role for baseline access. Add exceptional access later from Additional User Permissions.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -69,7 +78,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
$(form).on('submit', function(event) {
|
||||
$(form).off('submit.userCreate').on('submit.userCreate', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
$.post($(form).attr('action'), $(form).serialize())
|
||||
|
|
|
|||
|
|
@ -38,6 +38,18 @@
|
|||
<label class="form-label" for="phone_no">Phone No.</label>
|
||||
<input class="form-control" id="phone_no" name="phone_no" type="text" maxlength="20" value="<?= esc($formData['phone_no'] ?? ($phone_no ?? '')) ?>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="role">Role</label>
|
||||
<select class="form-select" id="role" name="role" required>
|
||||
<option value="">Select role</option>
|
||||
<?php foreach (($roles ?? []) as $roleKey => $roleInfo) : ?>
|
||||
<option value="<?= esc($roleKey) ?>" <?= (string) ($formData['role'] ?? ($current_role ?? '')) === (string) $roleKey ? 'selected' : '' ?>>
|
||||
<?= esc($roleInfo['title'] ?? $roleKey) ?>
|
||||
</option>
|
||||
<?php endforeach ?>
|
||||
</select>
|
||||
<div class="form-text">The role provides baseline access. Use Additional User Permissions only when this user needs extra access beyond the role.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="password">New Password (Optional)</label>
|
||||
<input class="form-control" id="password" name="password" type="password" autocomplete="new-password" placeholder="Leave blank to keep current password">
|
||||
|
|
@ -65,7 +77,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
$(form).on('submit', function(event) {
|
||||
$(form).off('submit.userEdit').on('submit.userEdit', function(event) {
|
||||
event.preventDefault();
|
||||
$.post($(form).attr('action'), $(form).serialize())
|
||||
.done(function(html) {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,17 @@
|
|||
<i class="bi bi-person-plus"></i>
|
||||
New User
|
||||
</button>
|
||||
<button class="btn btn-outline-primary" type="button" onclick="load_form_div('<?= base_url('setting/admin/user-management/permissions') ?>','maindiv','Give Permission');">
|
||||
<button class="btn btn-outline-dark" type="button" onclick="load_form_div('<?= base_url('setting/admin/roles') ?>','maindiv','Role Master');">
|
||||
<i class="bi bi-person-badge"></i>
|
||||
Role Master
|
||||
</button>
|
||||
<button class="btn btn-outline-primary" type="button" onclick="load_form_div('<?= base_url('setting/admin/user-management/permissions') ?>','maindiv','Additional Permissions');">
|
||||
<i class="bi bi-shield-lock"></i>
|
||||
Give Permission
|
||||
Additional Permissions
|
||||
</button>
|
||||
<button class="btn btn-outline-success" type="button" onclick="load_form_div('<?= base_url('setting/admin/user-management/sessions') ?>','maindiv','Active Sessions');">
|
||||
<i class="bi bi-person-check"></i>
|
||||
Who is Online
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -100,6 +108,9 @@
|
|||
<div class="d-flex gap-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="load_form_div('<?= base_url('setting/admin/user-management/edit/' . (int) ($user->id ?? 0)) ?>','maindiv','Edit User');">Edit</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-warning" onclick="load_form_div('<?= base_url('setting/admin/user-management/reset-password/' . (int) ($user->id ?? 0)) ?>','maindiv','Reset Password');">Reset Password</button>
|
||||
<?php if ((int) ($user->id ?? 0) !== (int) (auth()->user()->id ?? 0)) : ?>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger btn-delete-user" data-user-id="<?= (int) ($user->id ?? 0) ?>" data-username="<?= esc((string) ($user->username ?? ''), 'attr') ?>">Delete</button>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -141,5 +152,25 @@
|
|||
$('#um_filter_person,#um_filter_phone').val('');
|
||||
applyUserFilters();
|
||||
});
|
||||
|
||||
$(document).off('click.userDelete', '.btn-delete-user').on('click.userDelete', '.btn-delete-user', function() {
|
||||
var userId = parseInt($(this).data('user-id') || '0', 10);
|
||||
var username = ($(this).data('username') || 'this user').toString();
|
||||
if (userId <= 0 || !window.confirm('Delete ' + username + '? The account will no longer be able to sign in.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.post('<?= base_url('setting/admin/user-management/delete') ?>/' + userId, {
|
||||
'<?= csrf_token() ?>': $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>'
|
||||
}, function(data) {
|
||||
if (!data || parseInt(data.update || '0', 10) !== 1) {
|
||||
window.alert((data && data.error_text) || 'Unable to delete user.');
|
||||
return;
|
||||
}
|
||||
load_form_div('<?= base_url('setting/admin/user-management') ?>', 'maindiv', 'User Management');
|
||||
}, 'json').fail(function(xhr) {
|
||||
window.alert((xhr.responseJSON && xhr.responseJSON.error_text) || 'Unable to delete user.');
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h3 class="card-title mb-0">Give Permissions</h3>
|
||||
<div>
|
||||
<h3 class="card-title mb-0">Additional User Permissions</h3>
|
||||
<div class="text-muted small">Role permissions are inherited automatically. Use this page only for extra access needed by one user.</div>
|
||||
</div>
|
||||
<div class="card-tools ms-auto">
|
||||
<button class="btn btn-light" type="button" onclick="load_form_div('<?= base_url('setting/admin/user-management') ?>','maindiv','User Management');">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
|
|
@ -49,7 +52,15 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="form-label">Permissions</label>
|
||||
<?php if (! empty($selectedUser)) : ?>
|
||||
<div class="alert alert-secondary py-2 mb-3">
|
||||
<strong>Assigned role:</strong>
|
||||
<?= esc(! empty($selectedRoleTitles) ? implode(', ', $selectedRoleTitles) : 'No role') ?>.
|
||||
Permissions marked <span class="badge bg-secondary">Inherited from Role</span> are already available through this role.
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<label class="form-label">Direct grants</label>
|
||||
<div class="text-muted small mb-2">Checked boxes are additional permissions saved specifically for this user. Unchecking one does not remove access inherited from the role.</div>
|
||||
<div class="alert alert-info py-2 mb-2">
|
||||
Pharmacy module access is controlled by <strong>pharmacy.access</strong>. Editing past Walk-in/Registered pharmacy invoices requires <strong>pharmacy.invoice.edit-old</strong> (or <strong>pharmacy.invoice.admin</strong>). Discount caps can be granted with <strong>pharmacy.invoice.discount.10</strong>, <strong>pharmacy.invoice.discount.20</strong>, <strong>pharmacy.invoice.discount.30</strong>, or full override using <strong>pharmacy.invoice.admin</strong>. Base user discount can be set in hospital setting key <strong>PHARMACY_NORMAL_MAX_DISCOUNT_PERCENT</strong>.
|
||||
</div>
|
||||
|
|
@ -57,6 +68,7 @@
|
|||
<?php
|
||||
$selectedPermissions = [];
|
||||
$permissions = $permissions ?? [];
|
||||
$inheritedPermissions = $inheritedPermissions ?? [];
|
||||
if (! empty($selectedUser)) {
|
||||
$selectedPermissions = $selectedUser->getPermissions() ?? [];
|
||||
}
|
||||
|
|
@ -134,6 +146,9 @@
|
|||
<input class="form-check-input" type="checkbox" id="perm_<?= esc($permissionKey) ?>" name="permissions[]" value="<?= esc($permissionKey) ?>" <?= in_array($permissionKey, $selectedPermissions, true) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="perm_<?= esc($permissionKey) ?>">
|
||||
<?= esc($permissionLabel) ?>
|
||||
<?php if (in_array($permissionKey, $inheritedPermissions, true)) : ?>
|
||||
<span class="badge bg-secondary ms-1">Inherited from Role</span>
|
||||
<?php endif ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
|
|
@ -164,7 +179,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
loadButton.addEventListener('click', function() {
|
||||
$(document).off('click.userPermissionsLoad', '#loadPermissions').on('click.userPermissionsLoad', '#loadPermissions', function() {
|
||||
var userId = userSelect.value;
|
||||
if (!userId) {
|
||||
return;
|
||||
|
|
@ -177,7 +192,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
$(form).on('submit', function(event) {
|
||||
$(form).off('submit.userPermissions').on('submit.userPermissions', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
$.post($(form).attr('action'), $(form).serialize())
|
||||
|
|
|
|||
|
|
@ -67,11 +67,11 @@
|
|||
$('#pin_hint').text('PIN generated. Share this temporary PIN with user.');
|
||||
}
|
||||
|
||||
$('#btn_generate_pin').on('click', function() {
|
||||
$('#btn_generate_pin').off('click.userReset').on('click.userReset', function() {
|
||||
applyGeneratedPin(makeSixDigitPin());
|
||||
});
|
||||
|
||||
$('#btn_copy_pin').on('click', function() {
|
||||
$('#btn_copy_pin').off('click.userReset').on('click.userReset', function() {
|
||||
var pin = ($('#password').val() || '').toString();
|
||||
if (!/^\d{6}$/.test(pin)) {
|
||||
$('#pin_hint').text('Enter/generate valid 6-digit PIN first.');
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
|
||||
applyGeneratedPin(makeSixDigitPin());
|
||||
|
||||
$(form).on('submit', function(event) {
|
||||
$(form).off('submit.userReset').on('submit.userReset', function(event) {
|
||||
event.preventDefault();
|
||||
$.post($(form).attr('action'), $(form).serialize())
|
||||
.done(function(html) {
|
||||
|
|
|
|||
103
app/Views/Setting/Admin/UserManagement/sessions.php
Normal file
103
app/Views/Setting/Admin/UserManagement/sessions.php
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<div>
|
||||
<h3 class="card-title mb-0">Active HMS Sessions</h3>
|
||||
<div class="text-muted small">Users active within the last 2 minutes</div>
|
||||
</div>
|
||||
<div class="card-tools ms-auto d-flex gap-2">
|
||||
<button class="btn btn-outline-primary" type="button" id="btn_refresh_user_sessions">
|
||||
<i class="bi bi-arrow-clockwise"></i> Refresh
|
||||
</button>
|
||||
<button class="btn btn-light" type="button" onclick="load_form_div('<?= base_url('setting/admin/user-management') ?>','maindiv','User Management');">
|
||||
<i class="bi bi-arrow-left"></i> Back to Users
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php $sessions = $sessions ?? []; ?>
|
||||
<div class="alert alert-info py-2">
|
||||
<strong><?= count($sessions) ?></strong> user<?= count($sessions) === 1 ? '' : 's' ?> currently online. A user can have only one active HMS session.
|
||||
</div>
|
||||
<div id="user_session_notice"></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Login Time</th>
|
||||
<th>Last Activity</th>
|
||||
<th>IP Address</th>
|
||||
<th>Browser / Device</th>
|
||||
<th style="width:130px;">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($sessions === []) : ?>
|
||||
<tr><td colspan="6" class="text-center text-muted">No active users.</td></tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ($sessions as $row) : ?>
|
||||
<?php $isCurrent = (int) ($row['user_id'] ?? 0) === (int) ($current_user_id ?? 0); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= esc((string) ($row['person_name'] ?: ($row['username'] ?? ''))) ?></div>
|
||||
<div class="small text-muted"><?= esc((string) ($row['username'] ?? '')) ?></div>
|
||||
<?php if ($isCurrent) : ?><span class="badge bg-success">This session</span><?php endif ?>
|
||||
</td>
|
||||
<td><?= esc(date('d/m/Y h:i A', strtotime((string) ($row['login_at'] ?? 'now')))) ?></td>
|
||||
<td><?= esc(date('d/m/Y h:i:s A', strtotime((string) ($row['last_activity'] ?? 'now')))) ?></td>
|
||||
<td><?= esc((string) ($row['ip_address'] ?? '-')) ?></td>
|
||||
<td class="small text-break" style="max-width:320px;"><?= esc((string) ($row['user_agent'] ?? '-')) ?></td>
|
||||
<td>
|
||||
<?php if ($isCurrent) : ?>
|
||||
<span class="text-muted small">Use Sign Out</span>
|
||||
<?php else : ?>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm btn-force-user-logout" data-user-id="<?= (int) ($row['user_id'] ?? 0) ?>" data-user-name="<?= esc((string) ($row['username'] ?? 'user'), 'attr') ?>">
|
||||
Force Logout
|
||||
</button>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach ?>
|
||||
<?php endif ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var listUrl = '<?= base_url('setting/admin/user-management/sessions') ?>';
|
||||
|
||||
$(document)
|
||||
.off('click.userSessionsRefresh', '#btn_refresh_user_sessions')
|
||||
.on('click.userSessionsRefresh', '#btn_refresh_user_sessions', function() {
|
||||
load_form_div(listUrl, 'maindiv', 'Active Sessions');
|
||||
})
|
||||
.off('click.userSessionsLogout', '.btn-force-user-logout')
|
||||
.on('click.userSessionsLogout', '.btn-force-user-logout', function() {
|
||||
var button = this;
|
||||
var userId = parseInt(button.getAttribute('data-user-id') || '0', 10);
|
||||
var userName = button.getAttribute('data-user-name') || 'this user';
|
||||
if (userId <= 0 || !window.confirm('Force logout ' + userName + '?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
$.post('<?= base_url('setting/admin/user-management/sessions/force-logout') ?>/' + userId, {
|
||||
'<?= csrf_token() ?>': $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>'
|
||||
}, function(data) {
|
||||
if (!data || parseInt(data.update || '0', 10) !== 1) {
|
||||
$('#user_session_notice').html('<div class="alert alert-danger">' + $('<div>').text((data && data.error_text) || 'Unable to end session.').html() + '</div>');
|
||||
button.disabled = false;
|
||||
return;
|
||||
}
|
||||
load_form_div(listUrl, 'maindiv', 'Active Sessions');
|
||||
}, 'json').fail(function(xhr) {
|
||||
var message = (xhr.responseJSON && xhr.responseJSON.error_text) || 'Unable to end session.';
|
||||
$('#user_session_notice').html('<div class="alert alert-danger">' + $('<div>').text(message).html() + '</div>');
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<?php $canBillingSubmit = $user && method_exists($user, 'can') ? ($user->can('finance.cash.billing.submit') || $user->can('finance.*')) : false; ?>
|
||||
<?php $canAccountsVerify = $user && method_exists($user, 'can') ? ($user->can('finance.cash.accounts.accept') || $user->can('finance.cash.accounts.verify') || $user->can('finance.*')) : false; ?>
|
||||
<?php $canBankAudit = $user && method_exists($user, 'can') ? ($user->can('finance.bank.deposit.create') || $user->can('finance.bank.audit') || $user->can('finance.bank.statement.update') || $user->can('finance.*')) : false; ?>
|
||||
<?php $canMedicalStoreCredit = $user && method_exists($user, 'can') ? ($user->can('finance.workflow.view') || $user->can('finance.cash.accounts.accept') || $user->can('finance.cash.accounts.verify') || $user->can('finance.doctor_payout.manage') || $user->can('finance.*')) : false; ?>
|
||||
<?php $canMedicalStoreCredit = $user && method_exists($user, 'can') ? ($user->can('finance.workflow.view') || $user->can('finance.access') || $user->can('finance.cash.accounts.accept') || $user->can('finance.cash.accounts.verify') || $user->can('finance.doctor_payout.manage') || $user->can('finance.*')) : false; ?>
|
||||
|
||||
<div class="mb-3">
|
||||
<h2 class="mb-1">Accounts And Finance</h2>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,52 @@
|
|||
<link href="<?= base_url('assets/css/style.css') ?>" rel="stylesheet">
|
||||
<link href="<?= base_url('assets/css/custom-theme-dark-borders.css') ?>" rel="stylesheet">
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var checkingSession = false;
|
||||
var terminated = false;
|
||||
|
||||
function checkActiveSession() {
|
||||
if (checkingSession || terminated || document.hidden) {
|
||||
return;
|
||||
}
|
||||
checkingSession = true;
|
||||
|
||||
fetch('<?= base_url('auth/session-status') ?>', {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
cache: 'no-store',
|
||||
credentials: 'same-origin'
|
||||
}).then(function(response) {
|
||||
if (response.status !== 409) {
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
}).then(function(payload) {
|
||||
if (!payload || !payload.session_terminated || terminated) {
|
||||
return;
|
||||
}
|
||||
terminated = true;
|
||||
window.alert(payload.message || 'This login session was ended. Please sign in again.');
|
||||
window.location.assign(payload.login_url || '<?= base_url('login') ?>');
|
||||
}).catch(function() {
|
||||
}).finally(function() {
|
||||
checkingSession = false;
|
||||
});
|
||||
}
|
||||
|
||||
window.setInterval(checkActiveSession, 15000);
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (!document.hidden) {
|
||||
checkActiveSession();
|
||||
}
|
||||
});
|
||||
window.setTimeout(checkActiveSession, 3000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Early fallback so javascript:load_form(...) links never fail before full app scripts initialize.
|
||||
if (typeof window.load_form !== 'function') {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user