Your team): ?invite=&token=. The person checks their name and * chooses a password - nobody else ever knows it - and is made an agent with * the role, MFA and client access the invite set (userInviteAccept(), * functions/user_invite.php). Then they sign in on the usual page. A link * that has run out, been used or cancelled says so, and what to do instead. * * The client portal's own invites open client/join.php. */ header("Content-Security-Policy: default-src 'self'; img-src 'self' data:"); header("X-Frame-Options: DENY"); header("Referrer-Policy: no-referrer"); if (!file_exists('config.php')) { header("Location: /setup"); exit(); } require_once 'config.php'; require_once 'functions.php'; require_once 'includes/load_global_settings.php'; if (!isset($config_enable_setup) || $config_enable_setup == 1 || !userInvitesReady()) { header("Location: /login.php"); exit(); } // A password is chosen here - not over plain HTTP, as with signing in if ( $config_https_only && (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') && (!isset($_SERVER['HTTP_X_FORWARDED_PROTO']) || $_SERVER['HTTP_X_FORWARDED_PROTO'] !== 'https') ) { header("Location: /login.php"); exit(); } require_once __DIR__ . "/includes/session_init.php"; require_once "includes/inc_set_timezone.php"; // Nobody is signed in here - the history notes where it came from $session_ip = escapeSql(getIP()); $session_user_agent = escapeSql($_SERVER['HTTP_USER_AGENT'] ?? ''); $session_user_id = 0; $company = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT company_name, company_logo FROM companies WHERE company_id = 1")); $company_name = (string) ($company['company_name'] ?? ''); $app = userMailAppName(); $where = $company_name !== '' ? "$company_name on $app" : $app; $invite_id = intval($_POST['invite'] ?? ($_GET['invite'] ?? 0)); $token = $_POST['token'] ?? ($_GET['token'] ?? ''); $token = is_string($token) ? $token : ''; $link = userInviteByLink($invite_id, $token); $invite = $link['row']; $error = ''; $in_name = $invite ? (string) $invite['invite_name'] : ''; // Where they sign in - with the login key when one is required, for someone with a real invite $sign_in = '/login.php'; if ($invite && !empty($config_login_key_required)) { $sign_in .= '?key=' . rawurlencode((string) $config_login_key_secret); } if ($invite && $link['state'] === 'pending' && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['agent_join'])) { $in_name = (string) ($_POST['name'] ?? ''); $user_id = userInviteAccept($invite, $token, [ 'name' => $in_name, 'password' => (string) ($_POST['new_password'] ?? ''), 'confirm' => (string) ($_POST['confirm_password'] ?? ''), ], $error); if ($user_id) { $first = strtok(trim(preg_replace('/\s+/', ' ', strip_tags($in_name))), ' '); $_SESSION['login_message'] = "Welcome, $first - your account is ready. Sign in with " . $invite['invite_email'] . ' and your new password.' . (intval($invite['invite_force_mfa']) ? ' Have your authenticator app ready - you set up multi-factor authentication as you sign in.' : ''); $_SESSION['login_message_type'] = 'success'; header("Location: $sign_in"); exit(); } // Used or cancelled in the meantime - say so rather than show the form again $link = userInviteByLink($invite_id, $token); $invite = $link['row']; } $by = $invite ? userInviteBy($invite) : ''; $ask = $by !== '' ? "Ask $by to send it again." : 'Ask your administrator to send it again.'; $notes = [ 'gone' => ["That link doesn't work", 'It may have been copied only in part - or a newer invite replaced it, so use the link in the latest one.'], 'expired' => ['That invite has run out', 'Invites work for ' . USER_INVITE_LINK_DAYS . " days. $ask"], 'accepted' => ["You've joined already", 'Sign in with ' . ($invite['invite_email'] ?? 'your email address') . ' and the password you chose.'], 'cancelled' => ['That invite was cancelled', $by !== '' ? "Ask $by about it." : 'Ask your administrator about it.'], ]; $state = $link['state']; $title = $state === 'pending' ? "Join $where" : $notes[$state][0]; ?> <?= escapeHtml($company_name !== '' ? $company_name : $app) ?> | <?= escapeHtml($title) ?>