-
-
+
+
" . $row['user_name'] . " is the only active admin - $what would leave nobody to run ITFlow. Make someone else an admin first.", 'error');
+ redirect();
+ }
+}
+
if (isset($_POST['add_user'])) {
validateCSRFToken();
@@ -15,97 +30,57 @@ if (isset($_POST['add_user'])) {
redirect();
}
- require_once 'user_model.php';
-
- $password = password_hash(trim($_POST['password']), PASSWORD_DEFAULT);
- $user_specific_encryption_ciphertext = encryptUserSpecificKey(trim($_POST['password']));
-
- mysqli_query($mysqli, "INSERT INTO users SET user_name = '$name', user_email = '$email', user_password = '$password', user_specific_encryption_ciphertext = '$user_specific_encryption_ciphertext', user_role_id = $role");
+ $error = '';
+ $identity = userCheckIdentity($_POST, 0, $error);
+ $role = $identity ? userCheckRole($_POST['role'] ?? 0, 0, $error) : null;
+ $password = (string) ($_POST['password'] ?? '');
+ if ($role && ($problem = userPasswordProblem($password))) {
+ $error = $problem;
+ }
+ if (!$identity || !$role || $error !== '') {
+ flashAlert($error, 'error');
+ redirect();
+ }
+ [$name, $email] = $identity;
+ $role_id = intval($role['role_id']);
+ mysqli_query($mysqli, "INSERT INTO users SET user_name = '" . escapeSql($name) . "', user_email = '" . escapeSql($email) . "', user_password = '', user_role_id = $role_id");
$user_id = mysqli_insert_id($mysqli);
+ userSetPassword($user_id, $password);
- // Add Client Access (allow / deny per client)
- if (isset($_POST['client_permission'])) {
- foreach ($_POST['client_permission'] as $perm_client_id => $perm_type) {
- $perm_client_id = intval($perm_client_id);
- if ($perm_type === 'allow' || $perm_type === 'deny') {
- mysqli_query($mysqli, "INSERT INTO user_client_permissions SET user_id = $user_id, client_id = $perm_client_id, permission_type = '$perm_type'");
- }
- }
- }
-
- if (!file_exists("../uploads/users/$user_id/")) {
- mkdir("../uploads/users/$user_id");
- }
-
- // Check for and process image/photo
- $extended_alert_description = '';
- if (isset($_FILES['file']['tmp_name'])) {
- if ($new_file_name = checkFileUpload($_FILES['file'], array('jpg', 'jpeg', 'gif', 'png', 'webp'))) {
-
- $file_tmp_path = $_FILES['file']['tmp_name'];
-
- // directory in which the uploaded file will be moved
- $upload_file_dir = "../uploads/users/$user_id/";
- $dest_path = $upload_file_dir . $new_file_name;
- move_uploaded_file($file_tmp_path, $dest_path);
-
- // Set Avatar
- mysqli_query($mysqli, "UPDATE users SET user_avatar = '$new_file_name' WHERE user_id = $user_id");
- $extended_alert_description = '. File successfully uploaded.';
- }
- }
-
- // Create Settings
+ $force_mfa = !empty($_POST['force_mfa']) ? 1 : 0;
mysqli_query($mysqli, "INSERT INTO user_settings SET user_id = $user_id, user_config_force_mfa = $force_mfa");
- $sql = mysqli_query($mysqli,"SELECT company_name FROM companies WHERE company_id = 1");
- $row = mysqli_fetch_assoc($sql);
- $company_name = escapeSql($row['company_name']);
-
- // Sanitize Config vars from load_global_settings.php
- $config_mail_from_name = escapeSql($config_mail_from_name);
- $config_mail_from_email = escapeSql($config_mail_from_email);
- $config_ticket_from_email = escapeSql($config_ticket_from_email);
- $config_login_key_secret = mysqli_real_escape_string($mysqli, $config_login_key_secret);
- $config_base_url = escapeSql($config_base_url);
-
- // Send user e-mail, if specified
- if (isset($_POST['send_email']) && !empty($config_smtp_host) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
-
- $password = mysqli_real_escape_string($mysqli, $_POST['password']);
-
- // Only hand out the login key when the gate is actually enabled - same test as post/logout.php
- $login_url = "https://$config_base_url/login.php";
- if ($config_login_key_required == 1) {
- $login_url .= "?key=$config_login_key_secret";
- }
-
- $subject = "Your new $company_name ITFlow account";
- $body = "Hello $name,
An ITFlow account has been setup for you. Please change your password upon login.
Username: $email
Password: $password
Login URL: $login_url
--
$company_name - Support
$config_ticket_from_email";
-
- $data = [
- [
- 'from' => $config_mail_from_email,
- 'from_name' => $config_mail_from_name,
- 'recipient' => $email,
- 'recipient_name' => $name,
- 'subject' => $subject,
- 'body' => $body
- ]
- ];
- $mail = addToMailQueue($data);
-
- if ($mail !== true) {
- mysqli_query($mysqli, "INSERT INTO notifications SET notification_type = 'Mail', notification = 'Failed to send email to $email'");
- mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'Mail', log_action = 'Error', log_description = 'Failed to send email to $email regarding $subject. $mail', log_ip = '$session_ip', log_user_agent = '$session_user_agent', log_user_id = $session_user_id, log_entity_id = $user_id");
- }
-
+ if (!intval($role['role_is_admin'])) {
+ userSetClientAccess($user_id, $_POST['client_permission'] ?? []);
}
- logAudit("User", "Create", "$session_name created user $name", 0, $user_id);
+ $notes = [];
+ $avatar_error = '';
+ if (!empty($_FILES['avatar']['tmp_name']) && !userSaveAvatar($user_id, $_FILES['avatar'], $avatar_error)) {
+ $notes[] = $avatar_error . ' The user was created without one';
+ }
- flashAlert("User
$name created" . $extended_alert_description);
+ // Their sign in details by email - the password goes as typed, so it is theirs to change
+ if (!empty($_POST['send_email']) && !empty($config_smtp_host)) {
+ $company_name = (string) mysqli_fetch_row(mysqli_query($mysqli, "SELECT company_name FROM companies WHERE company_id = 1"))[0];
+ $login_url = "https://$config_base_url/login.php" . ($config_login_key_required == 1 ? "?key=$config_login_key_secret" : '');
+ $mail = userQueueMail($email, $name, "Your new $company_name ITFlow account",
+ 'Hello ' . escapeHtml($name) . ',
An ITFlow account has been set up for you. Please change your password after you sign in'
+ . ($force_mfa ? ', and have an authenticator app ready - you will set up multi-factor authentication the first time' : '') . '.
'
+ . 'Email: ' . escapeHtml($email) . '
Password: ' . escapeHtml($password) . '
Sign in at: ' . escapeHtml($login_url)
+ . '
--
' . escapeHtml($company_name) . ' - Support
' . escapeHtml($config_ticket_from_email));
+ if ($mail === true) {
+ $notes[] = 'their sign in details were emailed to them';
+ } else {
+ $notes[] = 'the email with their sign in details could not be queued';
+ logApp('Mail', 'error', 'Failed to queue the new account email for ' . $email . ": $mail");
+ }
+ }
+
+ logAudit("User", "Create", "$session_name created user " . escapeSql($name), 0, $user_id);
+
+ flashAlert("User
$name created" . ($notes ? ' - ' . implode('; ', $notes) : ''), $avatar_error ? 'warning' : 'success');
redirect();
@@ -115,80 +90,62 @@ if (isset($_POST['edit_user'])) {
validateCSRFToken();
- require_once 'user_model.php';
+ $row = userForRequest($_POST['user_id'] ?? 0);
+ $user_id = intval($row['user_id']);
- $user_id = intval($_POST['user_id']);
+ $error = '';
+ $identity = userCheckIdentity($_POST, $user_id, $error);
+ $role = $identity ? userCheckRole($_POST['role'] ?? 0, $row['user_role_id'], $error) : null;
+ $new_password = (string) ($_POST['new_password'] ?? '');
+ if ($role && $new_password !== '' && ($problem = userPasswordProblem($new_password))) {
+ $error = $problem;
+ }
+ if (!$identity || !$role || $error !== '') {
+ flashAlert($error, 'error');
+ redirect();
+ }
+ [$name, $email] = $identity;
+ if (intval($row['role_is_admin']) && !intval($role['role_is_admin'])) {
+ userGuardLastAdmin($row, 'a role without admin');
+ }
- $new_password = trim($_POST['new_password']);
+ mysqli_query($mysqli, "UPDATE users SET user_name = '" . escapeSql($name) . "', user_email = '" . escapeSql($email) . "', user_role_id = " . intval($role['role_id']) . " WHERE user_id = $user_id");
+ $force_mfa = !empty($_POST['force_mfa']) ? 1 : 0;
+ mysqli_query($mysqli, "INSERT INTO user_settings SET user_id = $user_id, user_config_force_mfa = $force_mfa ON DUPLICATE KEY UPDATE user_config_force_mfa = $force_mfa");
- // Update Client Access (allow / deny per client)
- mysqli_query($mysqli,"DELETE FROM user_client_permissions WHERE user_id = $user_id");
- if (isset($_POST['client_permission'])) {
- foreach ($_POST['client_permission'] as $perm_client_id => $perm_type) {
- $perm_client_id = intval($perm_client_id);
- if ($perm_type === 'allow' || $perm_type === 'deny') {
- mysqli_query($mysqli, "INSERT INTO user_client_permissions SET user_id = $user_id, client_id = $perm_client_id, permission_type = '$perm_type'");
- }
+ // An admin sees every client - their rules are left as they were, in case the role changes back
+ if (!intval($role['role_is_admin']) && isset($_POST['client_permission_shown'])) {
+ userSetClientAccess($user_id, $_POST['client_permission'] ?? []);
+ }
+
+ $changes = [];
+ if ($new_password !== '') {
+ userSetPassword($user_id, $new_password);
+ $changes[] = 'password changed';
+ userNotifyChange($name, $email, 'an administrator set a new password');
+ }
+ if (!empty($_POST['reset_mfa']) && trim((string) $row['user_token']) !== '') {
+ userResetMfa($user_id);
+ $changes[] = 'MFA reset';
+ userNotifyChange($name, $email, 'an administrator turned off multi-factor authentication' . ($force_mfa ? ' - you will set it up again at your next sign in' : ''));
+ }
+ if (strcasecmp($email, $row['user_email']) !== 0) {
+ $changes[] = 'email changed';
+ userNotifyChange($name, $row['user_email'], 'its email address is now ' . $email);
+ }
+ $avatar_error = '';
+ if (!empty($_POST['remove_avatar'])) {
+ userRemoveAvatar($user_id);
+ $changes[] = 'avatar removed';
+ } elseif (!empty($_FILES['avatar']['tmp_name'])) {
+ if (userSaveAvatar($user_id, $_FILES['avatar'], $avatar_error)) {
+ $changes[] = 'avatar updated';
}
}
- // Get current Avatar
- $sql = mysqli_query($mysqli, "SELECT user_avatar FROM users WHERE user_id = $user_id");
- $row = mysqli_fetch_assoc($sql);
- $existing_file_name = escapeSql($row['user_avatar']);
+ logAudit("User", "Edit", "$session_name edited user " . escapeSql($name) . ($changes ? ' - ' . escapeSql(implode(', ', $changes)) : ''), 0, $user_id);
- $extended_log_description = '';
- if (!empty($_POST['2fa'])) {
- $two_fa = $_POST['2fa'];
- }
-
- if (!file_exists("../uploads/users/$user_id/")) {
- mkdir("../uploads/users/$user_id");
- }
-
- // Check for and process image/photo
- $extended_alert_description = '';
- if (isset($_FILES['file']['tmp_name'])) {
- if ($new_file_name = checkFileUpload($_FILES['file'], array('jpg', 'jpeg', 'gif', 'png', 'webp'))) {
-
- $file_tmp_path = $_FILES['file']['tmp_name'];
-
- // directory in which the uploaded file will be moved
- $upload_file_dir = "../uploads/users/$user_id/";
- $dest_path = $upload_file_dir . $new_file_name;
- move_uploaded_file($file_tmp_path, $dest_path);
-
- // Delete old file
- unlink("../uploads/users/$user_id/$existing_file_name");
-
- // Set Avatar
- mysqli_query($mysqli, "UPDATE users SET user_avatar = '$new_file_name' WHERE user_id = $user_id");
- $extended_alert_description = '. File successfully uploaded.';
-
- }
- }
-
- mysqli_query($mysqli, "UPDATE users SET user_name = '$name', user_email = '$email', user_role_id = $role WHERE user_id = $user_id");
-
- if (!empty($new_password)) {
- $new_password = password_hash($new_password, PASSWORD_DEFAULT);
- $user_specific_encryption_ciphertext = encryptUserSpecificKey(trim($_POST['new_password']));
- mysqli_query($mysqli, "UPDATE users SET user_password = '$new_password', user_specific_encryption_ciphertext = '$user_specific_encryption_ciphertext' WHERE user_id = $user_id");
- //Extended Logging
- $extended_log_description .= ", password changed";
- }
-
- if (!empty($two_fa) && $two_fa == 'disable') {
- mysqli_query($mysqli, "UPDATE users SET user_token = '' WHERE user_id = '$user_id'");
- mysqli_query($mysqli, "INSERT INTO logs SET log_type = 'User', log_action = 'Modify', log_description = '$session_name disabled 2FA for $name', log_ip = '$session_ip', log_user_agent = '$session_user_agent', log_user_id = $session_user_id");
- }
-
- //Update User Settings
- mysqli_query($mysqli, "UPDATE user_settings SET user_config_force_mfa = $force_mfa WHERE user_id = $user_id");
-
- logAudit("User", "Edit", "$session_name edited user $name", 0, $user_id);
-
- flashAlert("User
$name updated" . $extended_alert_description);
+ flashAlert("User
$name saved" . ($changes ? ' - ' . implode(', ', $changes) : '') . ($avatar_error ? ". $avatar_error" : ''), $avatar_error ? 'warning' : 'success');
redirect();
@@ -198,22 +155,20 @@ if (isset($_GET['activate_user'])) {
validateCSRFToken();
- $user_id = intval($_GET['activate_user']);
+ $row = userForRequest($_GET['activate_user']);
+ $user_id = intval($row['user_id']);
// Only a user who isn't already using a seat can push the count over the limit
- $seat_row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(user_id) FROM users WHERE user_id = $user_id AND user_type = 1 AND user_status = 1 AND user_archived_at IS NULL"));
- if (intval($seat_row[0]) === 0 && agentUserLimitReached()) {
+ if ((intval($row['user_status']) !== 1 || $row['user_archived_at']) && agentUserLimitReached()) {
flashAlert(agentUserLimitMessage(), 'error');
redirect();
}
- $user_name = escapeSql(getFieldById('users', $user_id, 'user_name'));
-
mysqli_query($mysqli, "UPDATE users SET user_status = 1 WHERE user_id = $user_id");
- logAudit("User", "Activate", "$session_name activated user $user_name", 0, $user_id);
+ logAudit("User", "Activate", "$session_name activated user " . escapeSql($row['user_name']), 0, $user_id);
- flashAlert("User
$user_name activated");
+ flashAlert("User
" . $row['user_name'] . " can sign in again");
redirect();
@@ -223,30 +178,16 @@ if (isset($_GET['disable_user'])) {
validateCSRFToken();
- $user_id = intval($_GET['disable_user']);
-
- $user_name = escapeSql(getFieldById('users', $user_id, 'user_name'));
+ $row = userForRequest($_GET['disable_user']);
+ $user_id = intval($row['user_id']);
+ userGuardLastAdmin($row, 'disabling them');
mysqli_query($mysqli, "UPDATE users SET user_status = 0 WHERE user_id = $user_id");
+ userHandOverWork($user_id, 0, 'when ' . escapeSql($row['user_name']) . ' was disabled');
- // Un-assign tickets. Collect them first so each one gets a history entry -
- // otherwise a whole queue silently goes unassigned with nothing to explain it
- $affected_ticket_ids = array();
- $sql_affected_tickets = mysqli_query($mysqli, "SELECT ticket_id FROM tickets WHERE ticket_assigned_to = $user_id AND ticket_closed_at IS NULL");
- while ($affected_ticket_row = mysqli_fetch_assoc($sql_affected_tickets)) {
- $affected_ticket_ids[] = intval($affected_ticket_row['ticket_id']);
- }
+ logAudit("User", "Disable", "$session_name disabled user " . escapeSql($row['user_name']), 0, $user_id);
- mysqli_query($mysqli, "UPDATE tickets SET ticket_assigned_to = 0 WHERE ticket_assigned_to = $user_id AND ticket_closed_at IS NULL");
- mysqli_query($mysqli, "UPDATE recurring_tickets SET recurring_ticket_assigned_to = 0 WHERE recurring_ticket_assigned_to = $user_id");
-
- foreach ($affected_ticket_ids as $affected_ticket_id) {
- logTicketHistory($affected_ticket_id, "$session_name unassigned the ticket when $user_name was disabled");
- }
-
- logAudit("User", "Disable", "$session_name disabled user $user_name", 0, $user_id);
-
- flashAlert("User
$user_name disabled", 'error');
+ flashAlert("User
" . $row['user_name'] . " disabled - they are signed out, and their open tickets are unassigned", 'error');
redirect();
@@ -256,15 +197,32 @@ if (isset($_GET['revoke_remember_me'])) {
validateCSRFToken();
- $user_id = intval($_GET['revoke_remember_me']);
-
- $user_name = escapeSql(getFieldById('users', $user_id, 'user_name'));
+ $row = userForRequest($_GET['revoke_remember_me']);
+ $user_id = intval($row['user_id']);
mysqli_query($mysqli, "DELETE FROM remember_tokens WHERE remember_token_user_id = $user_id");
- logAudit("User", "Edit", "$session_name revoked all remember me tokens for user $user_name", 0, $user_id);
+ logAudit("User", "Edit", "$session_name revoked all remember me tokens for user " . escapeSql($row['user_name']), 0, $user_id);
- flashAlert("User
$user_name remember me tokens revoked", 'error');
+ flashAlert("Remembered browsers forgotten for
" . $row['user_name'] . " - every browser asks for their MFA code again");
+
+ redirect();
+
+}
+
+if (isset($_GET['reset_user_mfa'])) {
+
+ validateCSRFToken();
+
+ $row = userForRequest($_GET['reset_user_mfa']);
+ $user_id = intval($row['user_id']);
+
+ userResetMfa($user_id);
+ userNotifyChange($row['user_name'], $row['user_email'], 'an administrator turned off multi-factor authentication' . (intval($row['user_config_force_mfa']) ? ' - you will set it up again at your next sign in' : ''));
+
+ logAudit("User", "Edit", "$session_name reset MFA for user " . escapeSql($row['user_name']), 0, $user_id);
+
+ flashAlert("MFA reset for
" . $row['user_name'] . "" . (intval($row['user_config_force_mfa']) ? ' - they set it up again at their next sign in' : ' - they sign in with their password alone until they turn it on'));
redirect();
@@ -274,38 +232,26 @@ if (isset($_POST['archive_user'])) {
validateCSRFToken();
- $user_id = intval($_POST['user_id']);
- $ticket_assign = intval($_POST['ticket_assign']);
- $password = password_hash(randomString(), PASSWORD_DEFAULT);
+ $row = userForRequest($_POST['user_id'] ?? 0);
+ $user_id = intval($row['user_id']);
+ userGuardLastAdmin($row, 'archiving them');
- $user_name = escapeSql(getFieldById('users', $user_id, 'user_name'));
-
- // Un-assign / Re-assign tickets. Same as above - collect them first so the
- // move shows up on each ticket's history
- $affected_ticket_ids = array();
- $sql_affected_tickets = mysqli_query($mysqli, "SELECT ticket_id FROM tickets WHERE ticket_assigned_to = $user_id AND ticket_closed_at IS NULL AND ticket_resolved_at IS NULL");
- while ($affected_ticket_row = mysqli_fetch_assoc($sql_affected_tickets)) {
- $affected_ticket_ids[] = intval($affected_ticket_row['ticket_id']);
+ // Their work to an active agent, or to nobody
+ $to_user_id = intval($_POST['ticket_assign'] ?? 0);
+ if ($to_user_id && ($to_user_id === $user_id || !mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM users WHERE user_id = $to_user_id AND user_type = 1 AND user_status = 1 AND user_archived_at IS NULL")))) {
+ $to_user_id = 0;
}
+ userHandOverWork($user_id, $to_user_id, 'when ' . escapeSql($row['user_name']) . ' was archived');
- mysqli_query($mysqli, "UPDATE tickets SET ticket_assigned_to = $ticket_assign WHERE ticket_assigned_to = $user_id AND ticket_closed_at IS NULL AND ticket_resolved_at IS NULL");
- mysqli_query($mysqli, "UPDATE recurring_tickets SET recurring_ticket_assigned_to = $ticket_assign WHERE recurring_ticket_assigned_to = $user_id");
+ // They can no longer sign in, and their copy of the vault key is gone - restoring sets a new password
+ $archived_name = escapeSql(mb_substr($row['user_name'], 0, 189) . ' (archived)');
+ $scrambled = escapeSql(password_hash(randomString(), PASSWORD_DEFAULT));
+ mysqli_query($mysqli, "UPDATE users SET user_name = '$archived_name', user_password = '$scrambled', user_status = 0, user_specific_encryption_ciphertext = '', user_archived_at = NOW() WHERE user_id = $user_id");
+ mysqli_query($mysqli, "DELETE FROM remember_tokens WHERE remember_token_user_id = $user_id");
- $reassigned_to_name = $ticket_assign ? escapeSql(getFieldById('users', $ticket_assign, 'user_name')) : '';
- foreach ($affected_ticket_ids as $affected_ticket_id) {
- if ($reassigned_to_name) {
- logTicketHistory($affected_ticket_id, "$session_name reassigned the ticket to $reassigned_to_name when $user_name was archived");
- } else {
- logTicketHistory($affected_ticket_id, "$session_name unassigned the ticket when $user_name was archived");
- }
- }
+ logAudit("User", "Archive", "$session_name archived user " . escapeSql($row['user_name']), 0, $user_id);
- // Archive user query
- mysqli_query($mysqli, "UPDATE users SET user_name = '$user_name (archived)', user_password = '$password', user_status = 0, user_specific_encryption_ciphertext = '', user_archived_at = NOW() WHERE user_id = $user_id");
-
- logAudit("User", "Archive", "$session_name archived user $user_name", 0, $user_id);
-
- flashAlert("User
$user_name archived", 'error');
+ flashAlert("User
" . $row['user_name'] . " archived" . ($to_user_id ? ' - their open tickets went to ' . getFieldById('users', $to_user_id, 'user_name') : ''), 'error');
redirect();
@@ -315,34 +261,133 @@ if (isset($_POST['restore_user'])) {
validateCSRFToken();
- $user_id = intval($_POST['user_id']);
+ $row = userForRequest($_POST['user_id'] ?? 0);
+ $user_id = intval($row['user_id']);
- // Only a user who isn't already using a seat can push the count over the limit
- $seat_row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(user_id) FROM users WHERE user_id = $user_id AND user_type = 1 AND user_status = 1 AND user_archived_at IS NULL"));
- if (intval($seat_row[0]) === 0 && agentUserLimitReached()) {
+ if (agentUserLimitReached()) {
flashAlert(agentUserLimitMessage(), 'error');
redirect();
}
- $new_password = trim($_POST['new_password']);
- $role = intval($_POST['role']);
- $user_name = getFieldById('users', $user_id, 'user_name');
- $user_name = escapeSql(str_replace(" (archived)", "", $user_name)); //Removed (archived) from user_name
-
- // Restore user query
- mysqli_query($mysqli, "UPDATE users SET user_name = '$user_name', user_status = 1, user_role_id = $role, user_archived_at = NULL WHERE user_id = $user_id");
-
- if (!empty($new_password)) {
- $new_password = password_hash($new_password, PASSWORD_DEFAULT);
- $user_specific_encryption_ciphertext = encryptUserSpecificKey(trim($_POST['new_password']));
- mysqli_query($mysqli, "UPDATE users SET user_password = '$new_password', user_specific_encryption_ciphertext = '$user_specific_encryption_ciphertext' WHERE user_id = $user_id");
- //Extended Logging
- $extended_log_description .= ", password changed";
+ $error = '';
+ $role = userCheckRole($_POST['role'] ?? 0, $row['user_role_id'], $error);
+ $password = (string) ($_POST['new_password'] ?? '');
+ if ($role && ($problem = userPasswordProblem($password))) {
+ $error = $problem;
+ }
+ $name = trim(str_replace(' (archived)', '', $row['user_name']));
+ if ($role && mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM users WHERE user_email = '" . escapeSql($row['user_email']) . "' AND user_archived_at IS NULL AND user_id <> $user_id"))) {
+ $error = '
' . $row['user_email'] . ' signs in to another account now - change that one\'s email first.';
+ }
+ if (!$role || $error !== '') {
+ flashAlert($error, 'error');
+ redirect();
}
- logAudit("User", "Restored", "$session_name restored user $user_name", 0, $user_id);
+ mysqli_query($mysqli, "UPDATE users SET user_name = '" . escapeSql($name) . "', user_status = 1, user_role_id = " . intval($role['role_id']) . ", user_archived_at = NULL WHERE user_id = $user_id");
+ userSetPassword($user_id, $password);
- flashAlert("User
$user_name restored");
+ logAudit("User", "Restore", "$session_name restored user " . escapeSql($name), 0, $user_id);
+
+ flashAlert("User
$name restored - give them their new password");
+
+ redirect();
+
+}
+
+if (isset($_POST['bulk_role_users'])) {
+
+ validateCSRFToken();
+
+ $error = '';
+ $role = userCheckRole($_POST['role'] ?? 0, 0, $error);
+ if (!$role) {
+ flashAlert($error, 'error');
+ redirect();
+ }
+
+ $count = 0;
+ $kept = [];
+ foreach (usersForBulk() as $user_id => $row) {
+ if (intval($row['role_is_admin']) && !intval($role['role_is_admin']) && userIsLastAdmin($row)) {
+ $kept[] = $row['user_name'];
+ continue;
+ }
+ mysqli_query($mysqli, "UPDATE users SET user_role_id = " . intval($role['role_id']) . " WHERE user_id = $user_id");
+ logAudit("User", "Edit", "$session_name set the role of user " . escapeSql($row['user_name']) . " to " . escapeSql($role['role_name']), 0, $user_id);
+ $count++;
+ }
+
+ flashAlert("
$count user" . ($count === 1 ? '' : 's') . " now " . ($count === 1 ? 'has' : 'have') . " the
" . $role['role_name'] . " role" . ($kept ? ' - ' . implode(', ', $kept) . ' kept theirs, as the only admin' : ''), $kept ? 'warning' : 'success');
+
+ redirect();
+
+}
+
+if (isset($_POST['bulk_require_mfa_users']) || isset($_POST['bulk_unrequire_mfa_users'])) {
+
+ validateCSRFToken();
+
+ $force = isset($_POST['bulk_require_mfa_users']) ? 1 : 0;
+ $count = 0;
+ foreach (usersForBulk() as $user_id => $row) {
+ mysqli_query($mysqli, "INSERT INTO user_settings SET user_id = $user_id, user_config_force_mfa = $force ON DUPLICATE KEY UPDATE user_config_force_mfa = $force");
+ logAudit("User", "Edit", "$session_name " . ($force ? 'required' : 'stopped requiring') . " MFA for user " . escapeSql($row['user_name']), 0, $user_id);
+ $count++;
+ }
+
+ flashAlert(($force ? "MFA required for" : "MFA no longer required for") . "
$count user" . ($count === 1 ? '' : 's') . ($force ? ' - anyone without it sets it up at their next sign in' : ''));
+
+ redirect();
+
+}
+
+if (isset($_POST['bulk_revoke_remember_users'])) {
+
+ validateCSRFToken();
+
+ $count = 0;
+ foreach (usersForBulk() as $user_id => $row) {
+ mysqli_query($mysqli, "DELETE FROM remember_tokens WHERE remember_token_user_id = $user_id");
+ logAudit("User", "Edit", "$session_name revoked all remember me tokens for user " . escapeSql($row['user_name']), 0, $user_id);
+ $count++;
+ }
+
+ flashAlert("Remembered browsers forgotten for
$count user" . ($count === 1 ? '' : 's'));
+
+ redirect();
+
+}
+
+if (isset($_POST['bulk_disable_users']) || isset($_POST['bulk_activate_users'])) {
+
+ validateCSRFToken();
+
+ $activate = isset($_POST['bulk_activate_users']);
+ $count = 0;
+ $kept = [];
+ foreach (usersForBulk() as $user_id => $row) {
+ if ($row['user_archived_at'] || intval($row['user_status']) === ($activate ? 1 : 0)) {
+ continue;
+ }
+ if ($activate && agentUserLimitReached()) {
+ $kept[] = $row['user_name'];
+ continue;
+ }
+ if (!$activate && userIsLastAdmin($row)) {
+ $kept[] = $row['user_name'];
+ continue;
+ }
+ mysqli_query($mysqli, "UPDATE users SET user_status = " . ($activate ? 1 : 0) . " WHERE user_id = $user_id");
+ if (!$activate) {
+ userHandOverWork($user_id, 0, 'when ' . escapeSql($row['user_name']) . ' was disabled');
+ }
+ logAudit("User", $activate ? "Activate" : "Disable", "$session_name " . ($activate ? 'activated' : 'disabled') . " user " . escapeSql($row['user_name']), 0, $user_id);
+ $count++;
+ }
+
+ $why = $activate ? 'the agent user limit is reached' : 'the only admin';
+ flashAlert(($activate ? 'Activated' : 'Disabled') . "
$count user" . ($count === 1 ? '' : 's') . ($kept ? ' - not ' . implode(', ', $kept) . " ($why)" : ''), $kept ? 'warning' : ($activate ? 'success' : 'error'));
redirect();
@@ -359,7 +404,6 @@ if (isExportRequest('export_users')) {
// Filters inherited from the users page - mirrors admin/users.php
$filter_summary = [];
- // Archived Filter
if (isset($_POST['archived']) && $_POST['archived'] == 1) {
$archive_query = "user_archived_at IS NOT NULL";
$filter_summary['Archived'] = 'Archived only';
@@ -367,23 +411,33 @@ if (isExportRequest('export_users')) {
$archive_query = "user_archived_at IS NULL";
}
- // Search Filter
$q = escapeSql($_POST['q'] ?? '');
- if (!empty($q)) {
+ if ($q !== '') {
$filter_summary['Search'] = $_POST['q'];
}
- $sql = mysqli_query(
- $mysqli,
- "SELECT role_name, user_created_at, user_email, user_name, user_status FROM users
- LEFT JOIN user_roles ON user_role_id = role_id
- WHERE (user_name LIKE '%$q%' OR user_email LIKE '%$q%')
- AND user_type = 1
- AND $archive_query
- ORDER BY user_name ASC"
- );
+ $role_query = '';
+ $role = userRoles()[intval($_POST['role'] ?? 0)] ?? null;
+ if ($role) {
+ $role_query = 'AND user_role_id = ' . intval($role['role_id']);
+ $filter_summary['Role'] = $role['role_name'];
+ }
- $num_rows = mysqli_num_rows($sql);
+ $rows = [];
+ $sql = mysqli_query($mysqli, "SELECT users.user_id, role_name, role_is_admin, user_created_at, user_email, user_name, user_status, user_token, user_config_force_mfa FROM users
+ LEFT JOIN user_roles ON user_role_id = role_id
+ LEFT JOIN user_settings ON user_settings.user_id = users.user_id
+ WHERE (user_name LIKE '%$q%' OR user_email LIKE '%$q%' OR role_name LIKE '%$q%')
+ AND user_type = 1
+ AND $archive_query $role_query
+ ORDER BY user_name ASC");
+ while ($row = mysqli_fetch_assoc($sql)) {
+ $rows[intval($row['user_id'])] = $row;
+ }
+ $logins = userLastLogins(array_keys($rows));
+ $access = userClientAccess(array_keys($rows));
+
+ $num_rows = count($rows);
if ($num_rows > 0) {
@@ -391,17 +445,11 @@ if (isExportRequest('export_users')) {
$export = beginExport('users', $format, "$session_company_name-Users", 'Users', summarizeExportFilters($filter_summary));
- while ($row = mysqli_fetch_assoc($sql)) {
-
- $user_status = intval($row['user_status']);
- if ($user_status == 2) {
- $row['user_status_display'] = "Invited";
- } elseif ($user_status == 1) {
- $row['user_status_display'] = "Active";
- } else {
- $row['user_status_display'] = "Disabled";
- }
-
+ foreach ($rows as $user_id => $row) {
+ $row['user_status_display'] = USER_STATUSES[intval($row['user_status'])][0] ?? 'Disabled';
+ $row['user_mfa_display'] = trim((string) $row['user_token']) !== '' ? 'On' : (intval($row['user_config_force_mfa']) ? 'Required, not set up' : 'Off');
+ $row['user_last_login'] = $logins[$user_id]['log_created_at'] ?? 'Never';
+ $row['user_client_access'] = userClientAccessText($access[$user_id], intval($row['role_is_admin']));
addExportRow($export, $row);
}
@@ -416,40 +464,77 @@ if (isExportRequest('export_users')) {
if (isset($_POST['ir_reset_user_password'])) {
- // Incident response: allow mass reset of agent passwords
+ // Incident response: every other agent gets a new password, shown once here
validateCSRFToken();
// Confirm logged-in user password, for security
- $admin_password = $_POST['admin_password'];
- $sql = mysqli_query($mysqli, "SELECT user_password FROM users WHERE user_id = $session_user_id");
- $userRow = mysqli_fetch_assoc($sql);
-
- if (!password_verify($admin_password, $userRow['user_password'])) {
- flashAlert("Incorrect password.", 'error');
+ $user_row = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT user_password FROM users WHERE user_id = $session_user_id"));
+ if (!password_verify((string) ($_POST['admin_password'] ?? ''), $user_row['user_password'])) {
+ flashAlert("Incorrect password - nothing was reset.", 'error');
redirect();
}
- // Get agents/users, other than the current user
- $sql_users = mysqli_query($mysqli, "SELECT user_email, user_id FROM users WHERE (user_archived_at IS NULL AND user_id != $session_user_id)");
-
- // Reset passwords
+ // Agents only - client portal logins have their own reset, and no vault key to rewrite
+ $reset = [];
+ $sql_users = mysqli_query($mysqli, "SELECT user_id, user_name, user_email FROM users WHERE user_type = 1 AND user_archived_at IS NULL AND user_id <> $session_user_id ORDER BY user_name");
while ($row = mysqli_fetch_assoc($sql_users)) {
- $user_id = intval($row['user_id']);
- $user_email = escapeSql($row['user_email']);
- $new_password = randomString();
- $user_specific_encryption_ciphertext = encryptUserSpecificKey(trim($new_password));
-
- echo $user_email . " -- " . $new_password; // Show
- $new_password = password_hash($new_password, PASSWORD_DEFAULT);
-
- mysqli_query($mysqli, "UPDATE users SET user_password = '$new_password', user_specific_encryption_ciphertext = '$user_specific_encryption_ciphertext' WHERE user_id = $user_id");
-
- echo "
";
+ $new_password = generateReadablePassword(4);
+ userSetPassword($row['user_id'], $new_password);
+ // A remembered browser would skip their MFA code - in an incident, it must not
+ mysqli_query($mysqli, "DELETE FROM remember_tokens WHERE remember_token_user_id = " . intval($row['user_id']));
+ $reset[] = $row + ['password' => $new_password];
}
- logAudit("User", "Edit", "$session_name reset ALL user passwords");
+ logAudit("User", "Edit", "$session_name reset the passwords of " . count($reset) . " agent(s) and forgot their remembered browsers (incident response)");
- exit; // Stay on the plain text password page
+ // Shown once, not kept - a plain page to read out or copy, and nothing else on it
+ header('Cache-Control: no-store');
+ ?>
+
+
+
+
+
+
+
Incident response - new agent passwords
+
+
+
+
+
+
+
+
+
New passwords for = count($reset) ?> agent= count($reset) === 1 ? '' : 's' ?> - shown once
+
Give each one to its agent out of band (a phone call, not email), and have them change it. Their remembered browsers were forgotten, so MFA is asked for again. Leave this page and the passwords are gone.
+
+
+
+
+ | Agent | Email | New password |
+
+
+
+ | = escapeHtml($row['user_name']) ?> |
+ = escapeHtml($row['user_email']) ?> |
+ = escapeHtml($row['password']) ?> |
+
+
+
+ | There are no other agents. |
+
+
+
+
+
+
+
+
+ Users - the agents who sign in to ITFlow. Each row is who
+ * they are, their role, whether they can sign in and how (MFA, browsers that
+ * skip the code), the clients they can see and when they last signed in. The
+ * pills split them: everyone, admins, no MFA, disabled - and the archived.
+ *
+ * The admin's own row is shown but not changed here: that is their Account
+ * pages, so nobody locks themselves out by mistake.
+ */
+
// Default Column Sortby Filter
$sort = "user_name";
$order = "ASC";
require_once "includes/inc_all_admin.php";
-$sql = mysqli_query(
- $mysqli,
- "SELECT SQL_CALC_FOUND_ROWS role_name, user_archived_at, user_avatar, user_config_force_mfa, user_email,
- user_settings.user_id, user_name, user_role_id, user_status, user_token FROM users
- LEFT JOIN user_roles ON user_role_id = role_id
- LEFT JOIN user_settings ON users.user_id = user_settings.user_id
- WHERE (user_name LIKE '%$q%' OR user_email LIKE '%$q%')
- AND user_type = 1
- AND user_$archive_query
- ORDER BY $sort $order LIMIT $record_from, $record_to"
-);
+// filter_header.php fills in canned_date=custom when no range was asked for - not a filter here, so not carried into links
+if (($_GET['canned_date'] ?? '') === 'custom' && empty($_GET['dtf'])) {
+ unset($_GET['canned_date']);
+}
+
+$roles = userRoles();
+$role_filter = isset($roles[intval($_GET['role'] ?? 0)]) ? intval($_GET['role']) : 0;
+
+$filters = "user_type = 1 AND (user_name LIKE '%$q%' OR user_email LIKE '%$q%' OR role_name LIKE '%$q%')" . ($role_filter ? " AND user_role_id = $role_filter" : '');
+$joins = "LEFT JOIN user_roles ON user_role_id = role_id
+ LEFT JOIN user_settings ON user_settings.user_id = users.user_id";
+
+// The views
+$shows = [
+ '' => ['label' => 'Users', 'icon' => 'fa-users', 'where' => 'user_archived_at IS NULL', 'title' => 'Everyone not archived'],
+ 'admins' => ['label' => 'Admins', 'icon' => 'fa-user-shield', 'where' => 'user_archived_at IS NULL AND role_is_admin = 1', 'title' => 'Every client and every setting'],
+ 'nomfa' => ['label' => 'No MFA', 'icon' => 'fa-unlock', 'where' => "user_archived_at IS NULL AND user_status = 1 AND (user_token IS NULL OR user_token = '')", 'title' => 'Active, and signing in with a password alone'],
+ 'disabled' => ['label' => 'Disabled', 'icon' => 'fa-ban', 'where' => 'user_archived_at IS NULL AND user_status <> 1', 'title' => 'Cannot sign in'],
+ 'archived' => ['label' => 'Archived', 'icon' => 'fa-archive', 'where' => 'user_archived_at IS NOT NULL', 'title' => 'Gone - restore one with a new password'],
+];
+$show = isset($shows[$_GET['show'] ?? '']) ? ($_GET['show'] ?? '') : '';
+
+// Only these sort - an unknown column would take the page down
+$last_login_sql = "(SELECT MAX(log_id) FROM logs WHERE log_user_id = users.user_id AND log_type = 'Login' AND log_action = 'Success')";
+$sortable_columns = [
+ 'user_name' => "user_name $order",
+ 'user_email' => "user_email $order",
+ 'role_name' => "role_is_admin DESC, role_name $order, user_name ASC",
+ 'user_status' => "user_status $order, user_name ASC",
+ 'last_login' => "$last_login_sql IS NULL, $last_login_sql $order, user_name ASC",
+ 'user_created_at' => "user_created_at $order, user_name ASC",
+];
+if (!isset($sortable_columns[$sort])) {
+ $sort = 'user_name';
+}
+
+$sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS users.user_id, user_name, user_email, user_status, user_token, user_avatar, user_created_at, user_archived_at,
+ user_role_id, role_name, role_is_admin, user_config_force_mfa
+ FROM users $joins
+ WHERE $filters AND {$shows[$show]['where']}
+ ORDER BY {$sortable_columns[$sort]}, users.user_id ASC
+ LIMIT $record_from, $record_to");
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
+$users = [];
+while ($row = mysqli_fetch_assoc($sql)) {
+ $users[intval($row['user_id'])] = $row;
+}
+$logins = userLastLogins(array_keys($users));
+$access = userClientAccess(array_keys($users));
+$remembered = userRememberCounts(array_keys($users));
+
+// How many in each view, over what the search and role leave
+$view_counts = [];
+$row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', array_map(function ($s) {
+ return "COALESCE(SUM({$s['where']}), 0)";
+}, $shows)) . " FROM users $joins WHERE $filters"));
+foreach (array_keys($shows) as $i => $key) {
+ $view_counts[$key] = intval($row[$i]);
+}
+
// Agent user limit (set in config.php, 0 / unset = unlimited)
$agent_user_limit = getAgentUserLimit();
$agent_user_count = getActiveAgentUserCount();
$agent_user_limit_reached = $agent_user_limit > 0 && $agent_user_count >= $agent_user_limit;
+$filter_url = function ($changes) {
+ $params = array_merge($_GET, $changes);
+ unset($params['page']);
+ return '?' . http_build_query(array_filter($params, function ($v) { return $v !== '' && $v !== null; }));
+};
+
+$sort_link = function ($column, $label) use ($url_query_strings_sort, $sort, $disp, $order_icon) {
+ return '
'
+ . $label . ($sort === $column ? " $order_icon" : '') . '';
+};
+
+$csrf = $_SESSION['csrf_token'];
+
?>