Users > Client portal)
*
* The contacts who sign in to the client portal, at every client. What an
* admin does to one here is what their contact's page does, through the same
* functions (functions/portal.php): email a link to set a new password or the
* invite again, turn two-step sign-in off, forget the browsers that skip it,
* and turn their access off or back on. Never a password - they choose their
* own from a link. Each is in the audit log against the contact.
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
/** The portal user a request is about, or back to the list with why not. */
function portalUserForRequest($user_id) {
$row = portalUserRow($user_id);
if (!$row) {
flashAlert('That portal user no longer exists', 'error');
redirect();
}
return $row;
}
/** The portal users ticked in the list (user_ids[]) - [user_id => row]. */
function portalUsersForBulk() {
$rows = [];
foreach (array_unique(array_map('intval', (array) ($_POST['user_ids'] ?? []))) as $user_id) {
$row = portalUserRow($user_id);
if ($row) {
$rows[$user_id] = $row;
}
}
return $rows;
}
/** "3 people", "1 person". */
function portalUsersCount($count) {
return "$count " . ($count === 1 ? 'person' : 'people');
}
if (isset($_GET['portal_user_reset_link'])) {
// Email a link to choose a new password - an account that signs in with one
validateCSRFToken();
$row = portalUserForRequest($_GET['portal_user_reset_link']);
$name = escapeHtml($row['contact_name']);
if (!$row['portal_on'] || $row['user_auth_method'] !== 'local') {
flashAlert("$name " . ($row['portal_on'] ? 'signs in with Microsoft 365 - there is no password here to reset' : "can't sign in - " . lcfirst($row['portal_off_why'])), 'error');
redirect();
}
if (!portalMailPasswordReset($row['user_id'])) {
flashAlert("The link wasn't sent - " . (portalMailReady() ? 'their email address is not one mail can go to' : 'outgoing mail is not set up'), 'error');
redirect();
}
logAudit("Contact", "Edit", "$session_name emailed " . escapeSql($row['contact_name']) . " a link to reset their client portal password", $row['client_id'], $row['contact_id']);
flashAlert("$name was emailed a link to choose a new password - it works once, for " . PORTAL_RESET_LINK_MINUTES . " minutes");
redirect();
}
if (isset($_GET['portal_user_invite'])) {
// The invite again - a link to set their first password, or for Microsoft 365 the sign-in page
validateCSRFToken();
$row = portalUserForRequest($_GET['portal_user_invite']);
$name = escapeHtml($row['contact_name']);
if (!$row['portal_on']) {
flashAlert("$name can't sign in - " . lcfirst($row['portal_off_why']), 'error');
redirect();
}
if (!portalMailInvite($row['user_id'])) {
flashAlert("The invite wasn't sent - " . (portalMailReady() ? 'their email address is not one mail can go to' : 'outgoing mail is not set up'), 'error');
redirect();
}
logAudit("Contact", "Edit", "$session_name emailed " . escapeSql($row['contact_name']) . " a client portal invite", $row['client_id'], $row['contact_id']);
flashAlert("$name was emailed their invite to the client portal again");
redirect();
}
if (isset($_GET['portal_user_reset_mfa'])) {
// Two-step sign-in off - a lost or replaced phone
validateCSRFToken();
$row = portalUserForRequest($_GET['portal_user_reset_mfa']);
portalMfaReset($row['user_id']);
logAudit("Contact", "Edit", "$session_name turned off two-step sign-in for " . escapeSql($row['contact_name']) . " in the client portal", $row['client_id'], $row['contact_id']);
portalMailAccountChange($row['user_email'], $row['contact_name'], 'mfa_off_by_us');
flashAlert("Two-step sign-in is off for " . escapeHtml($row['contact_name']) . " - they sign in with their password alone until they set it up again");
redirect();
}
if (isset($_GET['portal_user_forget_browsers'])) {
validateCSRFToken();
$row = portalUserForRequest($_GET['portal_user_forget_browsers']);
$user_id = intval($row['user_id']);
mysqli_query($mysqli, "DELETE FROM remember_tokens WHERE remember_token_user_id = $user_id");
logAudit("Contact", "Edit", "$session_name forgot the remembered browsers of " . escapeSql($row['contact_name']) . " in the client portal", $row['client_id'], $row['contact_id']);
flashAlert("Remembered browsers forgotten for " . escapeHtml($row['contact_name']) . " - every browser asks for their code again");
redirect();
}
if (isset($_GET['portal_user_access_off'])) {
validateCSRFToken();
$row = portalUserForRequest($_GET['portal_user_access_off']);
portalUserAccessOff($row['user_id']);
logAudit("Contact", "Edit", "$session_name turned off client portal access for " . escapeSql($row['contact_name']), $row['client_id'], $row['contact_id']);
flashAlert("Portal access is off for " . escapeHtml($row['contact_name']) . " - they are signed out and can't sign in", 'error');
redirect();
}
if (isset($_POST['portal_user_access_on'])) {
// Back on, signing in the way picked - and the invite, when asked for
validateCSRFToken();
$row = portalUserForRequest($_POST['user_id'] ?? 0);
$name = escapeHtml($row['contact_name']);
$method = (string) ($_POST['method'] ?? 'local');
$error = portalUserAccessOn($row['user_id'], $method);
if ($error !== '') {
flashAlert("Portal access was not turned on for $name - " . escapeHtml($error), 'error');
redirect();
}
$invited = !empty($_POST['send_email']) ? portalMailInvite($row['user_id']) : null;
logAudit("Contact", "Edit", "$session_name turned on client portal access for " . escapeSql($row['contact_name']) . ($invited ? ' and emailed a client portal invite' : ''), $row['client_id'], $row['contact_id']);
flashAlert("Portal access is on for $name" . ($invited ? ' - they were emailed how to sign in' : ($invited === false ? " - the invite wasn't sent, as " . (portalMailReady() ? 'their email address is not one mail can go to' : 'outgoing mail is not set up') : '')),
$invited === false ? 'warning' : 'success');
redirect();
}
if (isset($_POST['bulk_portal_reset_links'])) {
validateCSRFToken();
$count = 0;
$skipped = 0;
foreach (portalUsersForBulk() as $user_id => $row) {
if (!$row['portal_on'] || $row['user_auth_method'] !== 'local' || !portalMailPasswordReset($user_id)) {
$skipped++;
continue;
}
logAudit("Contact", "Edit", "$session_name emailed " . escapeSql($row['contact_name']) . " a link to reset their client portal password", $row['client_id'], $row['contact_id']);
$count++;
}
flashAlert("Emailed " . portalUsersCount($count) . " a link to choose a new password" . ($skipped ? " - not $skipped who " . ($skipped === 1 ? 'signs' : 'sign') . " in with Microsoft 365, can't sign in, or can't be emailed" : ''), $skipped ? 'warning' : 'success');
redirect();
}
if (isset($_POST['bulk_portal_reset_mfa'])) {
validateCSRFToken();
$count = 0;
foreach (portalUsersForBulk() as $user_id => $row) {
if (trim((string) $row['user_token']) === '') {
continue;
}
portalMfaReset($user_id);
logAudit("Contact", "Edit", "$session_name turned off two-step sign-in for " . escapeSql($row['contact_name']) . " in the client portal", $row['client_id'], $row['contact_id']);
portalMailAccountChange($row['user_email'], $row['contact_name'], 'mfa_off_by_us');
$count++;
}
flashAlert("Two-step sign-in is off for " . portalUsersCount($count) . ($count ? ' - they sign in with their password alone until they set it up again' : ' - none of them had it on'), $count ? 'success' : 'warning');
redirect();
}
if (isset($_POST['bulk_portal_access_off'])) {
validateCSRFToken();
$count = 0;
foreach (portalUsersForBulk() as $user_id => $row) {
if (!in_array($row['user_auth_method'], ['local', 'azure'], true)) {
continue;
}
portalUserAccessOff($user_id);
logAudit("Contact", "Edit", "$session_name turned off client portal access for " . escapeSql($row['contact_name']), $row['client_id'], $row['contact_id']);
$count++;
}
flashAlert("Portal access is off for " . portalUsersCount($count) . ($count ? " - they are signed out and can't sign in" : ''), $count ? 'error' : 'warning');
redirect();
}