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));