From c263b9e4ac0205b183ea06fe136e25d90a1ea472 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Sun, 9 Aug 2026 23:21:16 +0530 Subject: [PATCH] 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. --- .../Authenticators/LegacySafeSession.php | 13 + app/Config/AuthGroups.php | 44 ++++ app/Config/Filters.php | 3 + app/Config/Routes.php | 245 +++++++++--------- app/Controllers/AuthSession.php | 21 ++ app/Controllers/Finance.php | 7 + app/Controllers/Setting/RoleManagement.php | 132 ++++++++++ app/Controllers/Setting/UserManagement.php | 245 +++++++++++++++++- ...9-000001_CreateUserActiveSessionsTable.php | 37 +++ ...-000002_UseInnoDbForUserActiveSessions.php | 20 ++ ...8-09-000003_CreateAuthRoleMasterTables.php | 74 ++++++ ...9-000004_AddDevelopedModulePermissions.php | 138 ++++++++++ app/Filters/SingleActiveSessionFilter.php | 50 ++++ app/Libraries/RoleRegistry.php | 219 ++++++++++++++++ app/Libraries/UserSessionRegistry.php | 183 +++++++++++++ .../Setting/Admin/RoleManagement/form.php | 83 ++++++ .../Setting/Admin/RoleManagement/index.php | 79 ++++++ .../Setting/Admin/UserManagement/create.php | 11 +- .../Setting/Admin/UserManagement/edit.php | 14 +- .../Setting/Admin/UserManagement/index.php | 35 ++- .../Admin/UserManagement/permissions.php | 23 +- .../Admin/UserManagement/reset_password.php | 6 +- .../Setting/Admin/UserManagement/sessions.php | 103 ++++++++ app/Views/finance/index.php | 2 +- app/Views/welcome_message.php | 46 ++++ 25 files changed, 1696 insertions(+), 137 deletions(-) create mode 100644 app/Controllers/AuthSession.php create mode 100644 app/Controllers/Setting/RoleManagement.php create mode 100644 app/Database/Migrations/2026-08-09-000001_CreateUserActiveSessionsTable.php create mode 100644 app/Database/Migrations/2026-08-09-000002_UseInnoDbForUserActiveSessions.php create mode 100644 app/Database/Migrations/2026-08-09-000003_CreateAuthRoleMasterTables.php create mode 100644 app/Database/Migrations/2026-08-09-000004_AddDevelopedModulePermissions.php create mode 100644 app/Filters/SingleActiveSessionFilter.php create mode 100644 app/Libraries/RoleRegistry.php create mode 100644 app/Libraries/UserSessionRegistry.php create mode 100644 app/Views/Setting/Admin/RoleManagement/form.php create mode 100644 app/Views/Setting/Admin/RoleManagement/index.php create mode 100644 app/Views/Setting/Admin/UserManagement/sessions.php diff --git a/app/Authentication/Authenticators/LegacySafeSession.php b/app/Authentication/Authenticators/LegacySafeSession.php index cd1398c..c114eeb 100644 --- a/app/Authentication/Authenticators/LegacySafeSession.php +++ b/app/Authentication/Authenticators/LegacySafeSession.php @@ -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 */ diff --git a/app/Config/AuthGroups.php b/app/Config/AuthGroups.php index bb7ed09..fafa0ca 100644 --- a/app/Config/AuthGroups.php +++ b/app/Config/AuthGroups.php @@ -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', diff --git a/app/Config/Filters.php b/app/Config/Filters.php index 58780fd..9f0ab6c 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -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', diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 0fbd527..ba95f52 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -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']); }); diff --git a/app/Controllers/AuthSession.php b/app/Controllers/AuthSession.php new file mode 100644 index 0000000..c7cec81 --- /dev/null +++ b/app/Controllers/AuthSession.php @@ -0,0 +1,21 @@ +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'), + ]); + } +} \ No newline at end of file diff --git a/app/Controllers/Finance.php b/app/Controllers/Finance.php index c552b42..3fdc0e8 100644 --- a/app/Controllers/Finance.php +++ b/app/Controllers/Finance.php @@ -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'); } diff --git a/app/Controllers/Setting/RoleManagement.php b/app/Controllers/Setting/RoleManagement.php new file mode 100644 index 0000000..8c3dfca --- /dev/null +++ b/app/Controllers/Setting/RoleManagement.php @@ -0,0 +1,132 @@ +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; + } +} diff --git a/app/Controllers/Setting/UserManagement.php b/app/Controllers/Setting/UserManagement.php index 3ab76b6..4a3b127 100644 --- a/app/Controllers/Setting/UserManagement.php +++ b/app/Controllers/Setting/UserManagement.php @@ -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, permissions: list} + */ + 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(), + ]); + } } diff --git a/app/Database/Migrations/2026-08-09-000001_CreateUserActiveSessionsTable.php b/app/Database/Migrations/2026-08-09-000001_CreateUserActiveSessionsTable.php new file mode 100644 index 0000000..dd657d1 --- /dev/null +++ b/app/Database/Migrations/2026-08-09-000001_CreateUserActiveSessionsTable.php @@ -0,0 +1,37 @@ +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); + } +} \ No newline at end of file diff --git a/app/Database/Migrations/2026-08-09-000002_UseInnoDbForUserActiveSessions.php b/app/Database/Migrations/2026-08-09-000002_UseInnoDbForUserActiveSessions.php new file mode 100644 index 0000000..7ff4d66 --- /dev/null +++ b/app/Database/Migrations/2026-08-09-000002_UseInnoDbForUserActiveSessions.php @@ -0,0 +1,20 @@ +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() + { + } +} \ No newline at end of file diff --git a/app/Database/Migrations/2026-08-09-000003_CreateAuthRoleMasterTables.php b/app/Database/Migrations/2026-08-09-000003_CreateAuthRoleMasterTables.php new file mode 100644 index 0000000..3b908e3 --- /dev/null +++ b/app/Database/Migrations/2026-08-09-000003_CreateAuthRoleMasterTables.php @@ -0,0 +1,74 @@ +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); + } +} diff --git a/app/Database/Migrations/2026-08-09-000004_AddDevelopedModulePermissions.php b/app/Database/Migrations/2026-08-09-000004_AddDevelopedModulePermissions.php new file mode 100644 index 0000000..73fb112 --- /dev/null +++ b/app/Database/Migrations/2026-08-09-000004_AddDevelopedModulePermissions.php @@ -0,0 +1,138 @@ +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; + } +} diff --git a/app/Filters/SingleActiveSessionFilter.php b/app/Filters/SingleActiveSessionFilter.php new file mode 100644 index 0000000..16e1c94 --- /dev/null +++ b/app/Filters/SingleActiveSessionFilter.php @@ -0,0 +1,50 @@ +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 + { + } +} \ No newline at end of file diff --git a/app/Libraries/RoleRegistry.php b/app/Libraries/RoleRegistry.php new file mode 100644 index 0000000..fc9fc01 --- /dev/null +++ b/app/Libraries/RoleRegistry.php @@ -0,0 +1,219 @@ +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; + } +} diff --git a/app/Libraries/UserSessionRegistry.php b/app/Libraries/UserSessionRegistry.php new file mode 100644 index 0000000..69da056 --- /dev/null +++ b/app/Libraries/UserSessionRegistry.php @@ -0,0 +1,183 @@ +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"); + } +} \ No newline at end of file diff --git a/app/Views/Setting/Admin/RoleManagement/form.php b/app/Views/Setting/Admin/RoleManagement/form.php new file mode 100644 index 0000000..c76d2b5 --- /dev/null +++ b/app/Views/Setting/Admin/RoleManagement/form.php @@ -0,0 +1,83 @@ + $label) { + $scope = strstr($key, '.', true) ?: 'other'; + $grouped[$scope][$key] = $label; +} +?> +
+
+

Users assigned this role inherit every selected permission.
+ +
+
+
+
Super Admin permissions are protected against lockout and cannot be changed.
+ +
+ +
+
+ + > +
Permanent lowercase system code; it cannot be changed later.
+
+
+ + +
+
+ + +
+
+ +
+
Inherited Permissions
Wildcard selections such as Pharmacy: All include current and future permissions in that module.
+ +
+
+ $items) : ?> +
+
+
+ $label) : ?> +
+ > + +
+ +
+
+ +
+ +
+ + +
+
+
+
+ + diff --git a/app/Views/Setting/Admin/RoleManagement/index.php b/app/Views/Setting/Admin/RoleManagement/index.php new file mode 100644 index 0000000..0c55c26 --- /dev/null +++ b/app/Views/Setting/Admin/RoleManagement/index.php @@ -0,0 +1,79 @@ +
+
+
+

Role Master

+
Define job roles and their inherited permissions.
+
+
+ + +
+
+
+
+ +
+ + +
+ + + + + + + + + + + + + + + +
RoleCodePermissionsUsersStatusTypeAction
Built-in' : 'Custom' ?> +
+ + + + + + + +
+
+
+
+
+ + diff --git a/app/Views/Setting/Admin/UserManagement/create.php b/app/Views/Setting/Admin/UserManagement/create.php index 13b1e07..73d14dd 100644 --- a/app/Views/Setting/Admin/UserManagement/create.php +++ b/app/Views/Setting/Admin/UserManagement/create.php @@ -33,6 +33,14 @@
Email is required.
+
+ + +
+
+ + +
@@ -51,6 +59,7 @@
Role is required.
+
Choose the closest job role for baseline access. Add exceptional access later from Additional User Permissions.
@@ -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()) diff --git a/app/Views/Setting/Admin/UserManagement/edit.php b/app/Views/Setting/Admin/UserManagement/edit.php index 43ab2f2..1534ea7 100644 --- a/app/Views/Setting/Admin/UserManagement/edit.php +++ b/app/Views/Setting/Admin/UserManagement/edit.php @@ -38,6 +38,18 @@ +
+ + +
The role provides baseline access. Use Additional User Permissions only when this user needs extra access beyond the role.
+
@@ -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) { diff --git a/app/Views/Setting/Admin/UserManagement/index.php b/app/Views/Setting/Admin/UserManagement/index.php index cfbee14..1924321 100644 --- a/app/Views/Setting/Admin/UserManagement/index.php +++ b/app/Views/Setting/Admin/UserManagement/index.php @@ -7,9 +7,17 @@ New User - + +
@@ -100,6 +108,9 @@
+ id ?? 0) !== (int) (auth()->user()->id ?? 0)) : ?> + +
@@ -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('/' + userId, { + '': $('input[name=""]').first().val() || '' + }, function(data) { + if (!data || parseInt(data.update || '0', 10) !== 1) { + window.alert((data && data.error_text) || 'Unable to delete user.'); + return; + } + load_form_div('', 'maindiv', 'User Management'); + }, 'json').fail(function(xhr) { + window.alert((xhr.responseJSON && xhr.responseJSON.error_text) || 'Unable to delete user.'); + }); + }); })(); \ No newline at end of file diff --git a/app/Views/Setting/Admin/UserManagement/permissions.php b/app/Views/Setting/Admin/UserManagement/permissions.php index fba3170..d2d1c0c 100644 --- a/app/Views/Setting/Admin/UserManagement/permissions.php +++ b/app/Views/Setting/Admin/UserManagement/permissions.php @@ -1,6 +1,9 @@
-

Give Permissions

+
+

Additional User Permissions

+
Role permissions are inherited automatically. Use this page only for extra access needed by one user.
+
- + +
+ Assigned role: + . + Permissions marked Inherited from Role are already available through this role. +
+ + +
Checked boxes are additional permissions saved specifically for this user. Unchecking one does not remove access inherited from the role.
Pharmacy module access is controlled by pharmacy.access. Editing past Walk-in/Registered pharmacy invoices requires pharmacy.invoice.edit-old (or pharmacy.invoice.admin). Discount caps can be granted with pharmacy.invoice.discount.10, pharmacy.invoice.discount.20, pharmacy.invoice.discount.30, or full override using pharmacy.invoice.admin. Base user discount can be set in hospital setting key PHARMACY_NORMAL_MAX_DISCOUNT_PERCENT.
@@ -57,6 +68,7 @@ getPermissions() ?? []; } @@ -134,6 +146,9 @@ >
@@ -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()) diff --git a/app/Views/Setting/Admin/UserManagement/reset_password.php b/app/Views/Setting/Admin/UserManagement/reset_password.php index cc5581a..8dfa050 100644 --- a/app/Views/Setting/Admin/UserManagement/reset_password.php +++ b/app/Views/Setting/Admin/UserManagement/reset_password.php @@ -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) { diff --git a/app/Views/Setting/Admin/UserManagement/sessions.php b/app/Views/Setting/Admin/UserManagement/sessions.php new file mode 100644 index 0000000..1f4fcd5 --- /dev/null +++ b/app/Views/Setting/Admin/UserManagement/sessions.php @@ -0,0 +1,103 @@ +
+
+
+

Active HMS Sessions

+
Users active within the last 2 minutes
+
+
+ + +
+
+
+ +
+ user currently online. A user can have only one active HMS session. +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
UserLogin TimeLast ActivityIP AddressBrowser / DeviceAction
No active users.
+
+
+ This session +
+ + Use Sign Out + + + +
+
+
+
+ + diff --git a/app/Views/finance/index.php b/app/Views/finance/index.php index 99a8f02..a01c893 100644 --- a/app/Views/finance/index.php +++ b/app/Views/finance/index.php @@ -3,7 +3,7 @@ can('finance.cash.billing.submit') || $user->can('finance.*')) : false; ?> can('finance.cash.accounts.accept') || $user->can('finance.cash.accounts.verify') || $user->can('finance.*')) : false; ?> can('finance.bank.deposit.create') || $user->can('finance.bank.audit') || $user->can('finance.bank.statement.update') || $user->can('finance.*')) : false; ?> - 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; ?> + 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; ?>

Accounts And Finance

diff --git a/app/Views/welcome_message.php b/app/Views/welcome_message.php index a6ed269..a7acfff 100644 --- a/app/Views/welcome_message.php +++ b/app/Views/welcome_message.php @@ -32,6 +32,52 @@ + +