Enforce roles on admin endpoints and return real status codes

- users, SMTP and batch-lock endpoints are owner/admin only
- engines answer 400/403/404/409/500 instead of 200 with an error body;
  database errors no longer leak to the client
This commit is contained in:
Thanakorn
2026-09-24 14:53:40 +07:00
parent 73c680e844
commit 8705be0d1b
35 changed files with 131 additions and 4 deletions
@@ -3,6 +3,9 @@ require_once __DIR__ . '/../../../session.php';
require_once '../../../assets/utils/db_auth.php';
require_once '../../../assets/utils/classes/OperationLockManager.php';
// Batch GL posting takes this lock; posting itself is owner/admin only.
require_role($user_role, ['owner', 'admin']);
try {
$lock_manager = new OperationLockManager($pdo2, $company_id, $user_id);
$result = $lock_manager->acquire(
@@ -10,7 +13,15 @@ try {
(int)($data['ttl_minutes'] ?? 120)
);
$answer = array_merge($answer, $result);
if (empty($result['success'])) {
http_response_code(409); // another tab or user holds the lock
}
} catch (PDOException $e) {
error_log('[acquire_op_lock] ' . $e->getMessage());
http_response_code(500);
$answer['message'] = 'Database error, please try again.';
} catch (Exception $e) {
http_response_code(400);
$answer['message'] = $e->getMessage();
}
@@ -8,6 +8,7 @@ $doc_type = trim((string)($data['doc_type'] ?? ''));
$source_id = (int)($data['source_id'] ?? 0);
if (!$doc_type || $source_id <= 0) {
http_response_code(400);
$answer['message'] = 'doc_type and source_id required.';
exit(json_encode($answer));
}
@@ -27,8 +28,13 @@ try {
$answer['success'] = 1;
$answer['output'] = $detail;
} catch (PDOException $e) {
error_log('[' . basename(__FILE__) . '] ' . $e->getMessage());
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
@@ -12,8 +12,13 @@ try {
(int)($data['formula_id'] ?? 0)
);
$answer['success'] = 1;
} catch (PDOException $e) {
error_log('[' . basename(__FILE__) . '] ' . $e->getMessage());
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
@@ -12,6 +12,7 @@ $to_date = trim((string)($data['to_date'] ?? ($data['to_period'] ??
$dept_id = (int)($data['department_id'] ?? 0);
if ($account_code === '') {
http_response_code(400);
$answer['message'] = 'account_code is required.';
exit(json_encode($answer));
}
@@ -7,8 +7,13 @@ try {
$gl_query = new GlQueryManager($pdo2, $company_id);
$answer['output'] = $gl_query->getJournalDetail((int)($data['gl_id'] ?? 0));
$answer['success'] = 1;
} catch (PDOException $e) {
error_log('[' . basename(__FILE__) . '] ' . $e->getMessage());
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
@@ -11,8 +11,13 @@ try {
trim((string)($data['date_to'] ?? ''))
);
$answer['success'] = 1;
} catch (PDOException $e) {
error_log('[' . basename(__FILE__) . '] ' . $e->getMessage());
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
@@ -3,12 +3,20 @@ require_once __DIR__ . '/../../../session.php';
require_once '../../../assets/utils/db_auth.php';
require_once '../../../assets/utils/classes/BatchActionManager.php';
// Logged at the end of a batch GL posting run, which is owner/admin only.
require_role($user_role, ['owner', 'admin']);
try {
$batch_action = new BatchActionManager($pdo2, $company_id, $user_id);
$batch_action->log($data);
$answer['success'] = 1;
$answer['message'] = 'Batch action logged.';
} catch (PDOException $e) {
error_log('[log_batch_action] ' . $e->getMessage());
http_response_code(500);
$answer['message'] = 'Database error, please try again.';
} catch (Exception $e) {
http_response_code(400);
$answer['message'] = $e->getMessage();
}
@@ -6,6 +6,7 @@ require_once '../../../assets/utils/classes_ac/ChartOfAccounts.php';
require_role($user_role, ['owner', 'admin']);
if (empty($data['account_code']) || empty($data['account_name']) || empty($data['account_type'])) {
http_response_code(400);
$answer['message'] = 'Account code, name, and type are required';
exit(json_encode($answer));
}
@@ -6,6 +6,7 @@ require_once '../../../assets/utils/classes_ac/DepartmentManager.php';
require_role($user_role, ['owner', 'admin']);
if (empty($data['dept_code']) || empty($data['dept_name'])) {
http_response_code(400);
$answer['message'] = 'Department code and name are required';
exit(json_encode($answer));
}
@@ -30,6 +30,7 @@ $posting_map = [
];
if (!isset($posting_map[$doc_type]) || $id <= 0) {
http_response_code(400);
$answer['message'] = 'Invalid doc_type or id.';
exit(json_encode($answer));
}
@@ -73,9 +74,15 @@ try {
'has_expense' => $has_expense,
], $company_id);
} catch (PDOException $e) {
if ($pdo2->inTransaction()) $pdo2->rollBack();
error_log('[' . basename(__FILE__) . '] ' . $e->getMessage());
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
if ($pdo2->inTransaction()) $pdo2->rollBack();
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
@@ -30,14 +30,17 @@ if ($data['action'] === 'save') {
$to = trim((string)($data['open_to'] ?? ''));
if ($from !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $from)) {
http_response_code(400);
$answer['message'] = 'Invalid open_from date. Use YYYY-MM-DD.';
exit(json_encode($answer));
}
if ($to !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $to)) {
http_response_code(400);
$answer['message'] = 'Invalid open_to date. Use YYYY-MM-DD.';
exit(json_encode($answer));
}
if ($from && $to && $from > $to) {
http_response_code(400);
$answer['message'] = 'Open From must be on or before Open To.';
exit(json_encode($answer));
}
@@ -50,5 +53,6 @@ if ($data['action'] === 'save') {
exit(json_encode($answer));
}
http_response_code(400);
$answer['message'] = 'Invalid action.';
exit(json_encode($answer));
@@ -3,12 +3,20 @@ require_once __DIR__ . '/../../../session.php';
require_once '../../../assets/utils/db_auth.php';
require_once '../../../assets/utils/classes/OperationLockManager.php';
// Batch GL posting takes this lock; posting itself is owner/admin only.
require_role($user_role, ['owner', 'admin']);
try {
$lock_manager = new OperationLockManager($pdo2, $company_id, $user_id);
$lock_manager->release(trim((string)($data['operation_type'] ?? '')));
$answer['success'] = 1;
$answer['message'] = 'Lock released.';
} catch (PDOException $e) {
error_log('[release_op_lock] ' . $e->getMessage());
http_response_code(500);
$answer['message'] = 'Database error, please try again.';
} catch (Exception $e) {
http_response_code(400);
$answer['message'] = $e->getMessage();
}
@@ -7,6 +7,7 @@ require_role($user_role, ['owner', 'admin']);
$id = (int)($data['id'] ?? 0);
if (!$id) {
http_response_code(400);
$answer['message'] = 'Missing id';
exit(json_encode($answer));
}
@@ -6,7 +6,7 @@ require_once '../../../assets/utils/classes_ac/DepartmentManager.php';
require_role($user_role, ['owner', 'admin']);
$id = (int)($data['id'] ?? 0);
if (!$id) { $answer['message'] = 'Missing id'; exit(json_encode($answer)); }
if (!$id) { http_response_code(400); $answer['message'] = 'Missing id'; exit(json_encode($answer)); }
$dept = new DepartmentManager($pdo2, $company_id);
$dept->delete($id);
@@ -5,6 +5,7 @@ require_once '../../../assets/utils/classes_ac/ChartOfAccounts.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
http_response_code(400);
$answer['message'] = 'Missing id';
exit(json_encode($answer));
}
@@ -12,6 +13,7 @@ if (!$id) {
$coa = new ChartOfAccounts($pdo2, $company_id);
$row = $coa->getById($id);
if (!$row) {
http_response_code(404);
$answer['message'] = 'Account not found';
exit(json_encode($answer));
}
@@ -4,11 +4,11 @@ require_once '../../../assets/utils/db_auth.php';
require_once '../../../assets/utils/classes_ac/DepartmentManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) { $answer['message'] = 'Missing id'; exit(json_encode($answer)); }
if (!$id) { http_response_code(400); $answer['message'] = 'Missing id'; exit(json_encode($answer)); }
$dept = new DepartmentManager($pdo2, $company_id);
$row = $dept->getById($id);
if (!$row) { $answer['message'] = 'Department not found'; exit(json_encode($answer)); }
if (!$row) { http_response_code(404); $answer['message'] = 'Department not found'; exit(json_encode($answer)); }
$answer['output'] = $row;
$answer['success'] = 1;
@@ -25,6 +25,7 @@ if (preg_match('#^(\d{2})/(\d{2})/(\d{4})$#', $journal_date, $m)) {
}
if (!$journal_date || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $journal_date)) {
http_response_code(400);
$answer['message'] = 'Valid journal date is required.';
exit(json_encode($answer));
}
@@ -51,11 +52,13 @@ foreach ($lines_raw as $l) {
}
if (count($lines) < 2) {
http_response_code(400);
$answer['message'] = 'At least two journal lines are required.';
exit(json_encode($answer));
}
if (abs($total_debit - $total_credit) > 0.005) {
http_response_code(400);
$answer['message'] = 'Journal is not balanced. Debit ' . number_format($total_debit, 2) . ' ≠ Credit ' . number_format($total_credit, 2) . '.';
exit(json_encode($answer));
}
@@ -83,9 +86,15 @@ try {
$answer['success'] = 1;
$answer['gl_id'] = $gl_id;
notify_node('gl_posted', gl_posted_payload('manual', (int)$gl_id, $event_action, $lines), $company_id);
} catch (PDOException $e) {
if ($pdo2->inTransaction()) $pdo2->rollBack();
error_log('[' . basename(__FILE__) . '] ' . $e->getMessage());
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
if ($pdo2->inTransaction()) $pdo2->rollBack();
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
@@ -20,6 +20,7 @@
$errors = $uploader->upload('contact_image');
if (!empty($errors)) {
$answer['success'] = 0;
http_response_code(400);
$answer['message'] = $errors[0];
exit(json_encode($answer));
}
@@ -12,6 +12,7 @@ $discount = (float)($data['discount'] ?? 0);
$shipping_fee = (float)($data['shipping_fee'] ?? 0);
if (!$request_id) {
http_response_code(400);
$answer['message'] = 'Purchase request ID is required.';
exit(json_encode($answer));
}
@@ -21,6 +22,7 @@ $prm = new PurchaseRequestManager($pdo2, $company_id);
// Load PR — must be Approved
$pr = $prm->getById($request_id);
if (!$pr || (int)$pr['status'] !== 2) {
http_response_code(400);
$answer['message'] = 'Purchase request not found or not in Approved status.';
exit(json_encode($answer));
}
@@ -30,17 +32,20 @@ if (!$contact_id) {
$contact_id = (int)($pr['contact_id'] ?? 0);
}
if (!$contact_id) {
http_response_code(400);
$answer['message'] = 'Set a Preferred Supplier on this purchase request before converting it to a PO.';
exit(json_encode($answer));
}
$pr_items = $pr['items'];
if (empty($pr_items)) {
http_response_code(400);
$answer['message'] = 'Purchase request has no items.';
exit(json_encode($answer));
}
if ((float)$pr['total_remaining'] <= 0.000001) {
http_response_code(400);
$answer['message'] = 'Purchase request is already fully converted.';
exit(json_encode($answer));
}
@@ -72,6 +77,7 @@ foreach ($convert_items as $ci) {
if ($qty <= 0) continue;
if (!isset($pr_by_id[$item_id])) {
http_response_code(400);
$answer['message'] = "Item #{$item_id} not found in this purchase request.";
exit(json_encode($answer));
}
@@ -81,6 +87,7 @@ foreach ($convert_items as $ci) {
if ($qty - $remaining > 0.000001) {
$name = $pi['product_name'] ?: $pi['product_sku'];
http_response_code(400);
$answer['message'] = "Cannot convert {$qty} for \"{$name}\": only {$remaining} remaining.";
exit(json_encode($answer));
}
@@ -105,6 +112,7 @@ foreach ($convert_items as $ci) {
}
if (empty($po_items)) {
http_response_code(400);
$answer['message'] = 'No valid items to convert.';
exit(json_encode($answer));
}
@@ -8,6 +8,7 @@ $id = (int)($data['id'] ?? 0);
$action = $data['action_type'] ?? '';
if (!$id || !$action) {
http_response_code(400);
$answer['message'] = 'ID and action are required.';
exit(json_encode($answer));
}
@@ -8,6 +8,7 @@
if (!$warehouse_id) {
$answer['success'] = 0;
http_response_code(400);
$answer['message'] = 'Warehouse is required';
exit(json_encode($answer));
}
@@ -37,6 +37,7 @@
$errors = $uploader->upload('product_image');
if (!empty($errors)) {
$answer['success'] = 0;
http_response_code(400);
$answer['message'] = $errors[0];
exit(json_encode($answer));
}
+5
View File
@@ -9,8 +9,13 @@
$users = new UserManager($pdo1, $company_id, $user_id);
$answer['output'] = $users->getCompanyUsers();
$answer['success'] = 1;
} catch (PDOException $e) {
error_log('[' . basename(__FILE__) . '] ' . $e->getMessage());
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
@@ -12,6 +12,7 @@
$to_month = $data['to_month'] ?? '';
if (!$warehouse_id || !$from_month || !$to_month) {
http_response_code(400);
$answer['message'] = 'warehouse_id, from_month, and to_month are required.';
exit(json_encode($answer));
}
@@ -13,6 +13,7 @@
$to_month = $data['to_month'] ?? '';
if (!$warehouse_id || !$product_sku || !$from_month || !$to_month) {
http_response_code(400);
$answer['message'] = 'warehouse_id, product_sku, from_month, and to_month are required.';
exit(json_encode($answer));
}
@@ -10,6 +10,7 @@ $quotation_id = (int)($data['quotation_id'] ?? 0);
$convert_items = $data['convert_items'] ?? [];
if (!$quotation_id) {
http_response_code(400);
$answer['message'] = 'Quotation ID required.';
exit(json_encode($answer));
}
@@ -19,17 +20,20 @@ $qm = new QuotationManager($pdo2, $company_id);
// Load quotation — must be Accepted
$q = $qm->getById($quotation_id);
if (!$q || (int)$q['status'] !== 2) {
http_response_code(400);
$answer['message'] = 'Quotation not found or not in Accepted status.';
exit(json_encode($answer));
}
$qt_items = $q['items'];
if (empty($qt_items)) {
http_response_code(400);
$answer['message'] = 'Quotation has no items.';
exit(json_encode($answer));
}
if ((float)$q['total_remaining'] <= 0.000001) {
http_response_code(400);
$answer['message'] = 'Quotation is already fully converted.';
exit(json_encode($answer));
}
@@ -61,6 +65,7 @@ foreach ($convert_items as $ci) {
if ($qty <= 0) continue;
if (!isset($qt_by_id[$item_id])) {
http_response_code(400);
$answer['message'] = "Item #{$item_id} not found in this quotation.";
exit(json_encode($answer));
}
@@ -70,6 +75,7 @@ foreach ($convert_items as $ci) {
if ($qty - $remaining > 0.000001) {
$name = $qi['product_name'] ?: $qi['product_sku'];
http_response_code(400);
$answer['message'] = "Cannot convert {$qty} for \"{$name}\": only {$remaining} remaining.";
exit(json_encode($answer));
}
@@ -95,6 +101,7 @@ foreach ($convert_items as $ci) {
}
if (empty($ord_items)) {
http_response_code(400);
$answer['message'] = 'No valid items to convert.';
exit(json_encode($answer));
}
+2
View File
@@ -10,11 +10,13 @@ $action = $data['action'] ?? '';
$data['items'] = json_decode($data['items'] ?? '[]', true) ?: [];
if (!in_array($action, ['create', 'update'], true)) {
http_response_code(400);
$answer['message'] = 'Unknown action';
exit(json_encode($answer));
}
if (empty($data['items'])) {
http_response_code(400);
$answer['message'] = 'At least one item is required';
exit(json_encode($answer));
}
@@ -7,8 +7,13 @@ try {
$quotation = new QuotationManager($pdo2, $company_id);
$answer['output'] = $quotation->getStats();
$answer['success'] = 1;
} catch (PDOException $e) {
error_log('[' . basename(__FILE__) . '] ' . $e->getMessage());
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
@@ -9,6 +9,7 @@ $id = (int)($data['id'] ?? 0);
$action = $data['action_type'] ?? '';
if (!$id || !$action) {
http_response_code(400);
$answer['message'] = 'ID and action are required.';
exit(json_encode($answer));
}
@@ -38,10 +38,16 @@ try {
break;
default:
http_response_code(400);
$answer['message'] = 'Unknown action.';
}
} catch (PDOException $e) {
error_log('[' . basename(__FILE__) . '] ' . $e->getMessage());
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
@@ -34,6 +34,7 @@ try {
if ($action === 'rebuild_period') {
$period = $data['period'] ?? '';
if (!preg_match('/^\d{4}-\d{2}$/', $period)) {
http_response_code(400);
$answer['message'] = 'Invalid period.';
exit(json_encode($answer));
}
+5 -1
View File
@@ -3,13 +3,17 @@
require_once '../../../assets/utils/db_auth.php';
require_once '../../../assets/utils/classes/SmtpManager.php';
// Admin data: owners and admins only; staff and viewers never see it.
require_role($user_role, ['owner', 'admin']);
try {
$smtp = new SmtpManager($pdo1, $company_id, $method, $pinkey, $iv);
$answer['success'] = 1;
$answer['output'] = $smtp->get();
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
error_log('[retrieve_smtp] ' . $e->getMessage());
$answer['message'] = 'Could not load SMTP settings.';
http_response_code(500);
}
@@ -3,6 +3,9 @@
require_once '../../../assets/utils/db_auth.php';
require_once '../../../assets/utils/classes/UserManager.php';
// Admin data: owners and admins only; staff and viewers never see it.
require_role($user_role, ['owner', 'admin']);
$um = new UserManager($pdo1, $company_id, $user_id);
$answer['success'] = 1;
+3
View File
@@ -3,6 +3,9 @@
require_once '../../../assets/utils/db_auth.php';
require_once '../../../assets/utils/classes/UserManager.php';
// Admin data: owners and admins only; staff and viewers never see it.
require_role($user_role, ['owner', 'admin']);
$keyword = trim($data['keyword'] ?? '');
$um = new UserManager($pdo1, $company_id, $user_id);
@@ -36,6 +36,7 @@ try {
if ($action === 'get_periods') {
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
if (!$warehouse_id) {
http_response_code(400);
$answer['message'] = 'warehouse_id is required.';
exit(json_encode($answer));
}
@@ -48,6 +49,7 @@ try {
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
$period = $data['period'] ?? '';
if (!$warehouse_id || !preg_match('/^\d{4}-\d{2}$/', $period)) {
http_response_code(400);
$answer['message'] = 'Valid warehouse_id and period (YYYY-MM) are required.';
exit(json_encode($answer));
}