- C1: verify.php now filters license='owner' — invite tokens no longer accepted - C1: onboarding API rejects non-owner sessions - C2: Existing-user invite requires explicit acceptance via accept_invite.php - C2: New accept_invite.php page and API engine added - C2: inviteUser() generates token + expiry for existing users; resendInvite() handles active users - C3: session_regenerate_id(true) before writing invite session keys on both invite pages - C4: invited_onboarding API wraps activation in transaction with SELECT FOR UPDATE; rowCount check added; SQLSTATE 23000 caught cleanly - C5: inviteUser() and resendInvite() two-table writes wrapped in transactions - M2: removeUser() wrapped in transaction with FOR UPDATE; clears default_company on active user removal - M4: Logged-in user guard added to invited_onboarding.php and accept_invite.php - M5: manage_users.php uses $server_url instead of HTTP_HOST for invite URLs - M6: Username regex enforces 3-32 chars; reserved name blocklist added - N5: searchUsers() changed from LIKE fuzzy search to exact email match only - N7: resendInvite() rate-limited to once per 60s via invite_resent_at column - Schema: company_map_user gains invite_expires_at and invite_resent_at columns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
159 lines
5.7 KiB
PHP
159 lines
5.7 KiB
PHP
<?php
|
|
require '../session.php';
|
|
require '../config.php';
|
|
require '../dbconn.php';
|
|
|
|
$token = trim($_GET['token'] ?? '');
|
|
|
|
if (!$token) {
|
|
header('Location: ' . $server_url . 'login/index.php');
|
|
exit;
|
|
}
|
|
|
|
// Reject if a user is already logged in — opening an invite link in an active
|
|
// session would bind invite state into the current session.
|
|
if (!empty($_SESSION['login_company_id'])) {
|
|
require '../include_header.php';
|
|
?>
|
|
<body>
|
|
<div class="container py-5" style="max-width:480px;">
|
|
<div class="text-center mb-5">
|
|
<a href="<?php echo $server_url?>login/index.php" class="d-inline-block mb-4">
|
|
<img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
|
|
<span class="ms-2"><img src="<?php echo $server_url?>assets/images/logo.svg" alt=""></span>
|
|
</a>
|
|
</div>
|
|
<div class="card text-center">
|
|
<div class="card-body p-5">
|
|
<i class="ti ti-user-check text-warning mb-3" style="font-size:3rem;"></i>
|
|
<h2 class="fs-4 mb-2">Already Signed In</h2>
|
|
<p class="text-muted mb-4">You are already signed in. Please sign out first before accepting an invitation.</p>
|
|
<a href="<?php echo $server_url?>login/index.php" class="btn btn-primary">Go to Dashboard</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
// Look up token — must exist in company_map_user and not be expired
|
|
$sth = $pdo1->prepare(
|
|
"SELECT m.map_id, m.role, m.invite_expires_at,
|
|
c.company_name,
|
|
u.email, u.name, u.surname
|
|
FROM company_map_user m
|
|
JOIN company_list c ON c.company_id = m.company_id
|
|
JOIN user u ON u.user_id = m.user_id
|
|
WHERE m.invite_token = :token
|
|
LIMIT 1"
|
|
);
|
|
$sth->execute([':token' => $token]);
|
|
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
|
|
|
$invite_error = null;
|
|
if (!$row) {
|
|
$invite_error = 'invalid';
|
|
} elseif ($row['invite_expires_at'] && strtotime($row['invite_expires_at']) <= time()) {
|
|
$invite_error = 'expired';
|
|
}
|
|
|
|
if ($invite_error) {
|
|
require '../include_header.php';
|
|
$msg = $invite_error === 'expired'
|
|
? ['icon' => 'ti-clock-x', 'title' => 'Invitation Expired',
|
|
'body' => 'This invitation link has expired. Please contact the company administrator to resend your invitation.']
|
|
: ['icon' => 'ti-user-x', 'title' => 'Invalid Invitation',
|
|
'body' => 'This invitation link is invalid or has already been used.'];
|
|
?>
|
|
<body>
|
|
<div class="container py-5" style="max-width:480px;">
|
|
<div class="text-center mb-5">
|
|
<a href="<?php echo $server_url?>login/index.php" class="d-inline-block mb-4">
|
|
<img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
|
|
<span class="ms-2"><img src="<?php echo $server_url?>assets/images/logo.svg" alt=""></span>
|
|
</a>
|
|
</div>
|
|
<div class="card text-center">
|
|
<div class="card-body p-5">
|
|
<i class="ti <?php echo $msg['icon']; ?> text-danger mb-3" style="font-size:3rem;"></i>
|
|
<h2 class="fs-4 mb-2"><?php echo $msg['title']; ?></h2>
|
|
<p class="text-muted mb-4"><?php echo $msg['body']; ?></p>
|
|
<a href="<?php echo $server_url?>login/index.php" class="btn btn-primary">Back to Sign In</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
// Regenerate session ID before binding invite identity to prevent session fixation
|
|
session_regenerate_id(true);
|
|
|
|
$_SESSION['accept_invite_token'] = $token;
|
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
|
|
$company_name = htmlspecialchars($row['company_name']);
|
|
$invite_email = htmlspecialchars($row['email']);
|
|
$invite_role = htmlspecialchars(ucfirst($row['role']));
|
|
|
|
require '../include_header.php';
|
|
?>
|
|
|
|
<body>
|
|
|
|
<div class="container py-5" style="max-width:480px;">
|
|
|
|
<div class="text-center mb-5">
|
|
<a href="<?php echo $server_url?>login/index.php" class="d-inline-block mb-4">
|
|
<img src="<?php echo $server_url?>assets/images/favicon.png" alt="" width="40"/>
|
|
<span class="ms-2"><img src="<?php echo $server_url?>assets/images/logo.svg" alt=""></span>
|
|
</a>
|
|
<h1 class="h4 mb-1">You've been invited!</h1>
|
|
<p class="text-muted">Accept the invitation to join <strong><?php echo $company_name ?></strong>.</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-body p-5 text-center">
|
|
<i class="ti ti-building mb-3 text-primary" style="font-size:3rem;"></i>
|
|
<h2 class="fs-5 mb-1"><?php echo $company_name ?></h2>
|
|
<p class="text-muted mb-1">You are invited as: <strong><?php echo $invite_role ?></strong></p>
|
|
<p class="text-muted small">Account: <?php echo $invite_email ?></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-end mt-4">
|
|
<button class="btn btn-primary px-5" id="btn_accept" onclick="accept_invite()">
|
|
<i class="ti ti-check me-1"></i>Accept Invitation
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
function accept_invite() {
|
|
const $btn = $('#btn_accept');
|
|
$btn.prop('disabled', true).html('<i class="ti ti-loader-2 me-1"></i>Accepting…');
|
|
|
|
ajax_request({
|
|
url: '<?php echo $server_url?>login/api/engine/accept_invite.php',
|
|
autoPrepare: false,
|
|
data: { json: JSON.stringify({}) },
|
|
onSuccess: function () {
|
|
bootbox.alert('Invitation accepted! You can now sign in.', function () {
|
|
window.location.href = '<?php echo $server_url?>login/index.php';
|
|
});
|
|
},
|
|
onError: function () {
|
|
$btn.prop('disabled', false).html('<i class="ti ti-check me-1"></i>Accept Invitation');
|
|
},
|
|
});
|
|
}
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|