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. * Invited is the people asked to join who aren't in yet: the invites waiting * or run out (functions/user_invite.php), to send again, get the link to * pass on, or cancel. A link made to pass on is shown once, at the top. * * Client portal is the other people who sign in: the contacts with a login to * the client portal, at every client (functions/portal.php). Each row is the * contact, their client, how they sign in and when they last did - to email * a password link or the invite again, turn two-step sign-in off, and turn * their access off or back on (admin/post/portal_users.php). They are made * from a client's Contacts page, or by a primary contact in the portal. * * 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"; // 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; // The search is for the side being looked at - agents and their invites, or portal users - and leaves the other side's counts alone $on_portal = ($_GET['show'] ?? '') === 'portal'; $agent_q = $on_portal ? '' : $q; $portal_q = $on_portal ? $q : ''; $filters = "user_type = 1 AND (user_name LIKE '%$agent_q%' OR user_email LIKE '%$agent_q%' OR role_name LIKE '%$agent_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'], 'invited' => ['label' => 'Invited', 'icon' => 'fa-paper-plane', 'where' => null, 'title' => 'Asked to join and not in yet - their invite waiting, or run out'], 'archived' => ['label' => 'Archived', 'icon' => 'fa-archive', 'where' => 'user_archived_at IS NOT NULL', 'title' => 'Gone - restore one with a new password'], 'portal' => ['label' => 'Client portal', 'icon' => 'fa-user-circle', 'where' => null, 'title' => 'The contacts who sign in to the client portal, at every client'], ]; if (!userInvitesReady()) { unset($shows['invited']); } $show = isset($shows[$_GET['show'] ?? '']) ? ($_GET['show'] ?? '') : ''; // Client portal - contacts, not agents, so a list of their own: narrowed to a client, and by how they sign in $portal_joins = "JOIN contacts ON contact_user_id = users.user_id JOIN clients ON client_id = contact_client_id"; $portal_never_sql = "NOT EXISTS (SELECT 1 FROM logs WHERE log_user_id = users.user_id AND log_type = 'Client Login' AND log_action = 'Success')"; $portal_accesses = [ '' => ['label' => 'Can sign in', 'icon' => 'fas fa-check-circle', 'where' => PORTAL_USER_ON_SQL], 'local' => ['label' => 'Password', 'icon' => 'fas fa-key', 'where' => PORTAL_USER_ON_SQL . " AND user_auth_method = 'local'"], 'azure' => ['label' => 'Microsoft 365', 'icon' => 'fab fa-microsoft', 'where' => PORTAL_USER_ON_SQL . " AND user_auth_method = 'azure'"], 'nomfa' => ['label' => 'No two-step', 'icon' => 'fas fa-unlock', 'where' => PORTAL_USER_ON_SQL . " AND user_auth_method = 'local' AND (user_token IS NULL OR user_token = '')"], 'never' => ['label' => 'Never signed in', 'icon' => 'fas fa-user-clock', 'where' => PORTAL_USER_ON_SQL . " AND $portal_never_sql"], 'off' => ['label' => 'Access off', 'icon' => 'fas fa-ban', 'where' => 'NOT (' . PORTAL_USER_ON_SQL . ')'], ]; $portal_access = isset($portal_accesses[$_GET['access'] ?? '']) ? ($_GET['access'] ?? '') : ''; $portal_client = intval($_GET['client'] ?? 0); $portal_filters = "user_type = 2 AND (contact_name LIKE '%$portal_q%' OR user_email LIKE '%$portal_q%' OR client_name LIKE '%$portal_q%')" . ($portal_client ? " AND client_id = $portal_client" : ''); // The invites - not users yet, so a list of their own $invite_filters = "invite_accepted_at IS NULL AND invite_cancelled_at IS NULL AND (invite_name LIKE '%$agent_q%' OR invite_email LIKE '%$agent_q%' OR role_name LIKE '%$agent_q%')" . ($role_filter ? " AND invite_role_id = $role_filter" : ''); $invite_joins = "LEFT JOIN user_roles ON role_id = invite_role_id LEFT JOIN users inviter ON inviter.user_id = invite_created_by AND invite_created_by > 0"; // 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", ]; // ...and the same headers sort the invites: when it runs out for Sign in, when it was sent for Added $invite_sortable_columns = [ 'user_name' => "invite_name $order", 'user_email' => "invite_email $order", 'role_name' => "role_is_admin DESC, role_name $order, invite_name ASC", 'user_status' => "invite_expires_at $order, invite_name ASC", 'user_created_at' => "invite_created_at $order, invite_name ASC", ]; // ...and the portal users: their contact's name, their client, how they sign in, when they last did $portal_last_login_sql = "(SELECT MAX(log_id) FROM logs WHERE log_user_id = users.user_id AND log_type = 'Client Login' AND log_action = 'Success')"; $portal_sortable_columns = [ 'user_name' => "contact_name $order", 'user_email' => "user_email $order, contact_name ASC", 'client_name' => "client_name $order, contact_name ASC", 'user_status' => "user_auth_method $order, (user_token IS NULL OR user_token = '') $order, contact_name ASC", 'last_login' => "$portal_last_login_sql IS NULL, $portal_last_login_sql $order, contact_name ASC", ]; if (!isset(($show === 'invited' ? $invite_sortable_columns : ($show === 'portal' ? $portal_sortable_columns : $sortable_columns))[$sort])) { $sort = 'user_name'; } $users = []; $invites = []; $portal_users = []; if ($show === 'portal') { $sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS users.user_id, user_email, user_auth_method, user_status, user_token, user_archived_at, user_created_at, contact_id, contact_name, contact_title, contact_photo, contact_archived_at, client_id, client_name, client_archived_at FROM users $portal_joins WHERE $portal_filters AND {$portal_accesses[$portal_access]['where']} ORDER BY {$portal_sortable_columns[$sort]}, users.user_id ASC LIMIT $record_from, $record_to"); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); while ($row = mysqli_fetch_assoc($sql)) { $portal_users[intval($row['user_id'])] = $row; } } elseif ($show === 'invited') { $sql = mysqli_query($mysqli, "SELECT SQL_CALC_FOUND_ROWS user_invites.*, role_name, role_is_admin, inviter.user_name AS invite_by_name FROM user_invites $invite_joins WHERE $invite_filters ORDER BY {$invite_sortable_columns[$sort]}, invite_id ASC LIMIT $record_from, $record_to"); $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()")); while ($row = mysqli_fetch_assoc($sql)) { $invites[] = $row; } } else { $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()")); 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)); // For the portal view: their last sign in, the browsers that skip their code, and the clients to narrow it to $portal_logins = portalLastSignIns(array_keys($portal_users)); $portal_remembered = userRememberCounts(array_keys($portal_users)); $portal_clients = []; if ($show === 'portal') { $sql = mysqli_query($mysqli, "SELECT client_id, client_name, client_abbreviation, COUNT(*) AS portal_users FROM users $portal_joins WHERE user_type = 2 GROUP BY client_id, client_name, client_abbreviation ORDER BY client_name ASC"); while ($row = mysqli_fetch_assoc($sql)) { $portal_clients[intval($row['client_id'])] = $row; } } $portal_mail_ready = portalMailReady(); $portal_two_step = function ($row) use ($config_client_portal_require_mfa) { // Two-step sign-in is theirs to turn on, unless the portal requires it - so no alarm for one without if (trim((string) $row['user_token']) !== '') { return 'Two-step'; } if (intval($config_client_portal_require_mfa)) { return 'Two-step pending'; } return 'No two-step'; }; // How many in each view, over what the search and role leave $user_views = array_filter($shows, function ($s) { return $s['where'] !== null; }); $view_counts = []; $row = mysqli_fetch_row(mysqli_query($mysqli, "SELECT " . implode(', ', array_map(function ($s) { return "COALESCE(SUM({$s['where']}), 0)"; }, $user_views)) . " FROM users $joins WHERE $filters")); foreach (array_keys($user_views) as $i => $key) { $view_counts[$key] = intval($row[$i]); } if (isset($shows['invited'])) { $view_counts['invited'] = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM user_invites $invite_joins WHERE $invite_filters"))[0]); } // Those who can sign in to the portal $view_counts['portal'] = intval(mysqli_fetch_row(mysqli_query($mysqli, "SELECT COUNT(*) FROM users $portal_joins WHERE $portal_filters AND " . PORTAL_USER_ON_SQL))[0]); // Agent user limit (set in config.php, 0 / unset = unlimited) - an invite waiting holds a seat $agent_user_limit = getAgentUserLimit(); $agent_user_count = getActiveAgentUserCount(); $agent_invites_holding = userInvitesHoldingSeats(); $agent_user_limit_reached = $agent_user_limit > 0 && $agent_user_count + $agent_invites_holding >= $agent_user_limit; // A managed instance - the user its manager signs in as for support, and the limit when the manager sets it $managed_support_id = managedSupportUserId(); $managed_limit_by = managedLocks()['seat_limit'] ? managedManagerName() : ''; // An invite link to pass on - shown this once $invite_link = userInviteTakeLink(); $invite_mail_ready = userInviteMailReady(); $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']; ?>
| = $sort_link('user_name', 'Invited') ?> = $sort_link('user_email', 'by email') ?> | = $sort_link('role_name', 'Role') ?> | = $sort_link('user_status', 'Invite') ?> | Clients | = $sort_link('user_created_at', 'Invited by') ?> | |
|---|---|---|---|---|---|
|
= $invite_name ?>
= escapeHtml($invite['invite_email']) ?>
= $invite_role ?>
= $invite_pill ?>
|
= $invite_role ?> |
= $invite_pill ?>
MFA required
= escapeHtml($invite_expired ? 'Ran out ' . date('M j', strtotime($invite['invite_expires_at'])) : $invite_label['title']) ?>
|
= escapeHtml(userClientAccessText($invite_access, $invite_admin)) ?> |
= escapeHtml(userInviteBy($invite) !== '' ? userInviteBy($invite) : 'Someone no longer here') ?> · = escapeHtml(timeAgo($invite['invite_created_at'])) ?>
Emailed = escapeHtml(timeAgo($invite['invite_sent_at'])) ?>= $invite_sent > 1 ? " · $invite_sent times" : '' ?>
A link to pass on
|
|