diff --git a/app/Domain/User/UserAdminService.php b/app/Domain/User/UserAdminService.php index ffe0215..4bb24c2 100644 --- a/app/Domain/User/UserAdminService.php +++ b/app/Domain/User/UserAdminService.php @@ -53,6 +53,7 @@ final class UserAdminService $result = ($this->users ?? new UserRecord())->validate($input); $existing = $existingUsers[array_search($id, array_map(static fn($row) => is_array($row) ? (int)($row['id'] ?? 0) : 0, $existingUsers), true)] ?? []; if ($this->isProtectedAdministrator(['id' => $id, ...$existing]) && array_key_exists('role_id', $input) && (int)$input['role_id'] !== 1) $result['errors']['role_id'] = 'The protected Administrator account cannot be reassigned.'; + if ($this->isProtectedAdministrator(['id' => $id, ...$existing]) && array_key_exists('is_active', $input) && !$this->asBool($input['is_active'])) $result['errors']['is_active'] = 'The protected Administrator account cannot be deactivated.'; $email = $result['email'] ?? ''; if (is_string($email) && $email !== '' && $this->hasDuplicateEmail($email, $existingUsers, $id)) { $result['errors']['email'] = 'Email address is already in use.'; diff --git a/public/index.php b/public/index.php index a7581b5..be62430 100644 --- a/public/index.php +++ b/public/index.php @@ -17,6 +17,7 @@ require_once __DIR__ . '/../app/Domain/SLA/SlaAgreement.php'; require_once __DIR__ . '/../app/Domain/SLA/SlaThresholdClassifier.php'; require_once __DIR__ . '/../app/Domain/User/PasswordPolicy.php'; require_once __DIR__ . '/../app/Domain/User/UserRecord.php'; +require_once __DIR__ . '/../app/Domain/User/UserAdminService.php'; require_once __DIR__ . '/../app/Domain/User/RoleRecord.php'; require_once __DIR__ . '/../app/Domain/User/PermissionMatrix.php'; require_once __DIR__ . '/../app/Domain/User/RolePermissionService.php'; @@ -658,6 +659,41 @@ if ($route === 'clients') { exit; } +if ($route === 'user_edit') { + require_permission('users.manage'); + if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { http_response_code(405); exit('Edit requires POST'); } + verify_csrf(); + $targetId = filter_var(scalar_input($_POST['user_id'] ?? null), FILTER_VALIDATE_INT); + $existingUsers = db()->query('SELECT id, name, email, role_id, is_active, role_name FROM users u JOIN roles r ON r.id = u.role_id')->fetchAll(); + $target = null; foreach ($existingUsers as $candidate) if ((int)$candidate['id'] === (int)$targetId) { $target = $candidate; break; } + if (!$target) { http_response_code(404); exit('User not found'); } + $payload = ['name' => $_POST['name'] ?? null, 'email' => $_POST['email'] ?? null, 'role_id' => $_POST['role_id'] ?? null, 'is_active' => isset($_POST['is_active']) ? '1' : '0']; + $service = new \App\Domain\User\UserAdminService(); + $result = $service->validateForEdit((int)$targetId, $payload, $existingUsers); + if (trim(scalar_input($_POST['password'] ?? null)) !== '') { $passwordCheck = $service->validatePasswordReset(['id' => $targetId], $_POST['password']); if (!$passwordCheck['valid']) $result['errors'] = [...$result['errors'], ...$passwordCheck['errors']]; } + $roleCheck = db()->prepare('SELECT id FROM roles WHERE id = :id'); $roleCheck->execute(['id' => $result['role_id'] ?? 0]); if (!$roleCheck->fetchColumn()) $result['errors']['role_id'] = 'Selected role does not exist.'; + if (!$result['errors']) { + $pdo = db(); $pdo->beginTransaction(); + $pdo->prepare('UPDATE users SET name = :name, email = :email, role_id = :role, is_active = :active WHERE id = :id')->execute(['name' => $result['name'], 'email' => $result['email'], 'role' => $result['role_id'], 'active' => $result['is_active'] ? 1 : 0, 'id' => $targetId]); + if (trim(scalar_input($_POST['password'] ?? null)) !== '') $pdo->prepare('UPDATE users SET password_hash = :hash WHERE id = :id')->execute(['hash' => password_hash(scalar_input($_POST['password']), PASSWORD_DEFAULT), 'id' => $targetId]); + audit('user_updated', 'user', (int)$targetId, ['password_changed' => trim(scalar_input($_POST['password'] ?? null)) !== '']); $pdo->commit(); header('Location: /?route=users&updated=1'); exit; + } + $_SESSION['user_edit_errors'] = $result['errors']; header('Location: /?route=users&edit_user=' . (int)$targetId); exit; +} + +if ($route === 'user_delete') { + if (($user['role_name'] ?? '') !== 'Administrator') { http_response_code(403); exit('Forbidden'); } + if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') { http_response_code(405); exit('Delete requires POST'); } + verify_csrf(); + $targetId = filter_var(scalar_input($_POST['user_id'] ?? null), FILTER_VALIDATE_INT); + if (!$targetId || (int)$targetId === (int)$user['id']) { http_response_code(400); exit('User cannot be deleted.'); } + $targetStmt = db()->prepare('SELECT u.id, u.name, u.role_id, r.name AS role_name FROM users u JOIN roles r ON r.id = u.role_id WHERE u.id = :id'); $targetStmt->execute(['id' => $targetId]); $target = $targetStmt->fetch(); + $service = new \App\Domain\User\UserAdminService(); + if (!$target || $service->isProtectedAdministrator($target)) { http_response_code(403); exit('Protected Administrator cannot be deleted.'); } + $linked = db()->prepare('SELECT (SELECT COUNT(*) FROM jobcard_assignments WHERE user_id = :id_a) + (SELECT COUNT(*) FROM time_entries WHERE technician_id = :id_b)'); $linked->execute(['id_a' => $targetId, 'id_b' => $targetId]); + if ((int)$linked->fetchColumn() > 0) { http_response_code(409); exit('User has assigned jobcards or time entries; deactivate the user instead.'); } + audit('user_deleted', 'user', (int)$targetId, ['name' => $target['name']]); db()->prepare('DELETE FROM users WHERE id = :id')->execute(['id' => $targetId]); header('Location: /?route=users&deleted=1'); exit; +} if ($route === 'users') { require_permission('users.manage'); $userErrors = []; @@ -682,10 +718,15 @@ if ($route === 'users') { } } $roles = db()->query('SELECT id, name FROM roles ORDER BY name')->fetchAll(); - $users = db()->query('SELECT u.id, u.name, u.email, u.is_active, u.last_login_at, r.name AS role_name FROM users u JOIN roles r ON r.id = u.role_id ORDER BY u.name')->fetchAll(); + $users = db()->query('SELECT u.id, u.name, u.email, u.role_id, u.is_active, u.last_login_at, r.name AS role_name FROM users u JOIN roles r ON r.id = u.role_id ORDER BY u.name')->fetchAll(); + $editUser = null; + $editUserId = filter_var(scalar_input($_GET['edit_user'] ?? null), FILTER_VALIDATE_INT); + foreach ($users as $listed) if ($editUserId && (int)$listed['id'] === (int)$editUserId) { $editUser = $listed; break; } + if (isset($_SESSION['user_edit_errors'])) { $userErrors = (array)$_SESSION['user_edit_errors']; unset($_SESSION['user_edit_errors']); } render_header('Users'); - echo '
Create and review system accounts.
| Name | Role | Status | Last login | ||
|---|---|---|---|---|---|
| ' . e($listedUser['name']) . ' | ' . e($listedUser['email']) . ' | ' . e($listedUser['role_name']) . ' | ' . e($listedUser['is_active'] ? 'Active' : 'Inactive') . ' | ' . e((string)($listedUser['last_login_at'] ?? 'Never')) . ' |
| Name | Role | Status | Last login | Actions | |
|---|---|---|---|---|---|
| ' . e($listedUser['name']) . ' | ' . e($listedUser['email']) . ' | ' . e($listedUser['role_name']) . ' | ' . e($listedUser['is_active'] ? 'Active' : 'Inactive') . ' | ' . e((string)($listedUser['last_login_at'] ?? 'Never')) . ' | Edit ' . (($user['role_name'] === 'Administrator' && (int)$listedUser['id'] !== (int)$user['id'] && $listedUser['role_name'] !== 'Administrator') ? '' : '') . ' |