- 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>
91 lines
3.4 KiB
PHP
91 lines
3.4 KiB
PHP
<?php
|
||
/**
|
||
* accept_invite.php — Accept a company invitation for an existing user.
|
||
*
|
||
* Called by accept_invite.php page AJAX after the user clicks Accept.
|
||
* The user already has an active account; this just clears the invite_token
|
||
* on their company_map_user row, making them a full member.
|
||
*
|
||
* Full flow:
|
||
* 1. Session guard — rejects if accept_invite_token is missing.
|
||
* 2. CSRF check.
|
||
* 3. Re-validate token against DB (not expired, invite_token still set).
|
||
* 4. Clear invite_token and invite_expires_at from company_map_user.
|
||
* 5. Verify exactly one row was updated.
|
||
* 6. Clear session keys.
|
||
* 7. Return { success: 1 }.
|
||
*/
|
||
|
||
require_once '../../../session.php';
|
||
require_once '../../../config.php';
|
||
require_once '../../../preset.php';
|
||
define('UNAUTHENTICATED_ROUTE', true);
|
||
require_once '../../../assets/utils/db_auth.php';
|
||
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
|
||
$answer = ['success' => 0, 'message' => ''];
|
||
|
||
// ── Step 1: Session guard ─────────────────────────────────────────────────────
|
||
if (empty($_SESSION['accept_invite_token'])) {
|
||
$answer['message'] = 'Invalid session. Please use your invitation link.';
|
||
http_response_code(403);
|
||
exit(json_encode($answer));
|
||
}
|
||
|
||
$token = $_SESSION['accept_invite_token'];
|
||
|
||
// ── Step 2: CSRF check ────────────────────────────────────────────────────────
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$csrf = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||
if (empty($csrf) || $csrf !== ($_SESSION['csrf_token'] ?? '')) {
|
||
http_response_code(403);
|
||
$answer['message'] = 'Invalid request.';
|
||
exit(json_encode($answer));
|
||
}
|
||
}
|
||
|
||
try {
|
||
|
||
// ── Step 3: Re-validate token ─────────────────────────────────────────────
|
||
$sth = $pdo1->prepare(
|
||
"SELECT map_id FROM company_map_user
|
||
WHERE invite_token = :token
|
||
AND invite_expires_at > NOW()
|
||
LIMIT 1"
|
||
);
|
||
$sth->execute([':token' => $token]);
|
||
if (!$sth->fetchColumn()) {
|
||
$answer['message'] = 'Invitation has expired or already been used.';
|
||
http_response_code(403);
|
||
exit(json_encode($answer));
|
||
}
|
||
|
||
// ── Step 4–5: Activate membership ────────────────────────────────────────
|
||
$stmt = $pdo1->prepare(
|
||
"UPDATE company_map_user
|
||
SET invite_token = NULL,
|
||
invite_expires_at = NULL
|
||
WHERE invite_token = :token"
|
||
);
|
||
$stmt->execute([':token' => $token]);
|
||
|
||
if ($stmt->rowCount() !== 1) {
|
||
$answer['message'] = 'Invitation is no longer valid.';
|
||
http_response_code(403);
|
||
exit(json_encode($answer));
|
||
}
|
||
|
||
// ── Step 6: Clear session keys ────────────────────────────────────────────
|
||
unset($_SESSION['accept_invite_token']);
|
||
|
||
$answer['success'] = 1;
|
||
$answer['message'] = 'Invitation accepted.';
|
||
|
||
} catch (Exception $e) {
|
||
$answer['message'] = $e->getMessage();
|
||
http_response_code(400);
|
||
}
|
||
|
||
exit(json_encode($answer));
|