" . $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(); $error = ''; $identity = userCheckIdentity($_POST, 0, $error); $role = $identity ? userCheckRole($_POST['role'] ?? 0, 0, $error) : null; if (!$identity || !$role) { flashAlert($error, 'error'); redirect(); } [$name, $email] = $identity; $role_id = intval($role['role_id']); $force_mfa = !empty($_POST['force_mfa']) ? 1 : 0; // An invite - they choose their own password - or a password set now. Without the // choice (an older form, a script), a password sent is one to set. $password = (string) ($_POST['password'] ?? ''); $how = ($_POST['sign_in'] ?? ($password !== '' ? 'password' : 'invite')) === 'invite' && userInvitesReady() ? 'invite' : 'password'; // An invite waiting for them holds a seat already - it is this one $open = userInviteFor($email); if (agentUserLimitReached($open && userInviteState($open) === 'pending' ? intval($open['invite_id']) : 0)) { flashAlert(agentUserLimitMessage(), 'error'); redirect(); } if ($how === 'invite') { $saved = userInviteSave(['name' => $name, 'email' => $email, 'role_id' => $role_id, 'force_mfa' => $force_mfa, 'access' => intval($role['role_is_admin']) ? [] : ($_POST['client_permission'] ?? [])], $_FILES['avatar'] ?? null); $invite_id = $saved['invite_id']; $mail = userInviteMailReady(); $send_error = ''; $sent = userInviteSend($invite_id, $mail, $send_error); if (!$sent) { if (!$saved['again']) { userInviteRemoveAvatar($invite_id); mysqli_query($mysqli, "DELETE FROM user_invites WHERE invite_id = $invite_id"); } flashAlert($send_error, 'error'); redirect(); } logAudit("User", "Invite", "$session_name invited " . escapeSql("$name ($email) as {$role['role_name']}") . ($saved['again'] ? ' again' : '') . ($mail ? '' : ' - a link to pass on')); $notes = []; if ($saved['again']) { $notes[] = 'the invite they had is changed to this, and its link no longer works'; } if ($saved['avatar_error'] !== '') { $notes[] = $saved['avatar_error'] . ' The invite went without one'; } if ($mail) { flashAlert("Invited $name - an email with a link to join is on the way to $email. It works for " . USER_INVITE_LINK_DAYS . " days" . ($notes ? ' - ' . implode('; ', $notes) : ''), $saved['avatar_error'] !== '' ? 'warning' : 'success'); } else { userInviteShowLink($invite_id, $sent['url']); } redirect('users.php?show=invited'); } if ($problem = userPasswordProblem($password)) { flashAlert($problem, 'error'); redirect(); } 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); mysqli_query($mysqli, "INSERT INTO user_settings SET user_id = $user_id, user_config_force_mfa = $force_mfa"); if (!intval($role['role_is_admin'])) { userSetClientAccess($user_id, $_POST['client_permission'] ?? []); } $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'; } // Set up here, the invite waiting for them is done with if ($open && userInviteCancel($open['invite_id'])) { $notes[] = 'the invite waiting for them is cancelled'; } logAudit("User", "Create", "$session_name created user " . escapeSql($name), 0, $user_id); flashAlert("User $name created - give them their password yourself" . ($notes ? ' - ' . implode('; ', $notes) : ''), $avatar_error ? 'warning' : 'success'); redirect(); } if (isset($_GET['resend_user_invite']) || isset($_GET['link_user_invite']) || isset($_GET['cancel_user_invite'])) { // An invite waiting: email it again, get its link to pass on (either makes a new link, // so the old one stops working), or cancel it validateCSRFToken(); $what = isset($_GET['resend_user_invite']) ? 'resend' : (isset($_GET['link_user_invite']) ? 'link' : 'cancel'); $invite = userInviteRow($_GET[$what . '_user_invite'] ?? 0); if (!$invite || !in_array(userInviteState($invite), ['pending', 'expired'], true)) { flashAlert("That invite isn't waiting any more - it may have been accepted or cancelled", 'error'); redirect(); } $invite_id = intval($invite['invite_id']); $who = escapeSql("{$invite['invite_name']} ({$invite['invite_email']})"); if ($what === 'cancel') { userInviteCancel($invite_id); logAudit("User", "Cancel", "$session_name cancelled the invite to $who"); flashAlert("Cancelled the invite to " . escapeHtml($invite['invite_email']) . " - its link no longer works"); redirect(); } // One that has run out holds no seat - sending it again takes one if (userInviteState($invite) === 'expired' && agentUserLimitReached()) { flashAlert(agentUserLimitMessage(), 'error'); redirect(); } if ($what === 'resend' && !userInviteMailReady()) { flashAlert("Mail isn't set up, so the invite can't be emailed - get its link and send it to them yourself", 'error'); redirect(); } $send_error = ''; $sent = userInviteSend($invite_id, $what === 'resend', $send_error); if (!$sent) { flashAlert($send_error, 'error'); redirect(); } if ($what === 'resend') { logAudit("User", "Invite", "$session_name sent the invite to $who again"); flashAlert("Sent the invite to " . escapeHtml($invite['invite_email']) . " again - it works for " . USER_INVITE_LINK_DAYS . " days, and the link in the last one no longer works"); } else { logAudit("User", "Invite", "$session_name got a new invite link for $who, to pass on"); userInviteShowLink($invite_id, $sent['url']); } redirect(); } if (isset($_POST['edit_user'])) { validateCSRFToken(); $row = userForRequest($_POST['user_id'] ?? 0); $user_id = intval($row['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'); } 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"); // 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'; } } logAudit("User", "Edit", "$session_name edited user " . escapeSql($name) . ($changes ? ' - ' . escapeSql(implode(', ', $changes)) : ''), 0, $user_id); flashAlert("User $name saved" . ($changes ? ' - ' . implode(', ', $changes) : '') . ($avatar_error ? ". $avatar_error" : ''), $avatar_error ? 'warning' : 'success'); redirect(); } if (isset($_GET['activate_user'])) { validateCSRFToken(); $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 if ((intval($row['user_status']) !== 1 || $row['user_archived_at']) && agentUserLimitReached()) { flashAlert(agentUserLimitMessage(), 'error'); redirect(); } mysqli_query($mysqli, "UPDATE users SET user_status = 1 WHERE user_id = $user_id"); logAudit("User", "Activate", "$session_name activated user " . escapeSql($row['user_name']), 0, $user_id); flashAlert("User " . $row['user_name'] . " can sign in again"); redirect(); } if (isset($_GET['disable_user'])) { validateCSRFToken(); $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'); logAudit("User", "Disable", "$session_name disabled user " . escapeSql($row['user_name']), 0, $user_id); flashAlert("User " . $row['user_name'] . " disabled - they are signed out, and their open tickets are unassigned", 'error'); redirect(); } if (isset($_GET['revoke_remember_me'])) { validateCSRFToken(); $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 " . escapeSql($row['user_name']), 0, $user_id); 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(); } if (isset($_POST['archive_user'])) { validateCSRFToken(); $row = userForRequest($_POST['user_id'] ?? 0); $user_id = intval($row['user_id']); userGuardLastAdmin($row, 'archiving them'); // 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'); // 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"); logAudit("User", "Archive", "$session_name archived user " . escapeSql($row['user_name']), 0, $user_id); flashAlert("User " . $row['user_name'] . " archived" . ($to_user_id ? ' - their open tickets went to ' . getFieldById('users', $to_user_id, 'user_name') : ''), 'error'); redirect(); } if (isset($_POST['restore_user'])) { validateCSRFToken(); $row = userForRequest($_POST['user_id'] ?? 0); $user_id = intval($row['user_id']); if (agentUserLimitReached()) { flashAlert(agentUserLimitMessage(), 'error'); redirect(); } $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(); } 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); 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(); } if (isExportRequest('export_users')) { validateCSRFToken(); enforceAdminPermission(); $format = resolveExportFormat($_POST['export_users']); // Filters inherited from the users page - mirrors admin/users.php $filter_summary = []; if (isset($_POST['archived']) && $_POST['archived'] == 1) { $archive_query = "user_archived_at IS NOT NULL"; $filter_summary['Archived'] = 'Archived only'; } else { $archive_query = "user_archived_at IS NULL"; } $q = escapeSql($_POST['q'] ?? ''); if ($q !== '') { $filter_summary['Search'] = $_POST['q']; } $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']; } $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); guardExportHasRows($num_rows, 'users'); if ($num_rows > 0) { guardExportPdfRowCount($format, $num_rows); $export = beginExport('users', $format, "$session_company_name-Users", 'Users', summarizeExportFilters($filter_summary)); 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); } finishExport($export); } logAudit("User", "Export", "$session_name exported $num_rows user(s) to a " . strtoupper($format) . " file"); exit; } if (isset($_POST['ir_reset_user_password'])) { // Incident response: every other agent gets a new password, shown once here validateCSRFToken(); // Confirm logged-in user password, for security $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(); } // 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)) { $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 the passwords of " . count($reset) . " agent(s) and forgot their remembered browsers (incident response)"); // 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 agent - 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.
AgentEmailNew password
There are no other agents.
Back to Users