Files
wms-app/app/login/api/engine/accept_invite.php
T
Thanakorn SandClaude Sonnet 4.6 b4b1f5cbec Security hardening: invited user onboarding flow (C1–N7)
- 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>
2026-05-26 10:18:40 +07:00

91 lines
3.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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));