Security hardening: invited user onboarding flow (C1–N7)

This commit is contained in:
Thanakorn S
2026-05-26 10:18:40 +07:00
parent 0815ae3292
commit 9e200d31fe
9 changed files with 562 additions and 163 deletions
+90
View File
@@ -0,0 +1,90 @@
<?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));