0, 'message' => '']; // ── Step 1: Session guard ───────────────────────────────────────────────────── if (empty($_SESSION['invited_user_id']) || empty($_SESSION['invited_token'])) { $answer['message'] = 'Invalid session. Please use your invitation link.'; http_response_code(403); exit(json_encode($answer)); } $user_id = (int)$_SESSION['invited_user_id']; $token = $_SESSION['invited_token']; // ── Step 2: CSRF check ──────────────────────────────────────────────────────── if ($_SERVER['REQUEST_METHOD'] === 'POST') { $csrf = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; if (empty($csrf) || !hash_equals((string)($_SESSION['csrf_token'] ?? ''), $csrf)) { http_response_code(403); $answer['message'] = 'Invalid request.'; exit(json_encode($answer)); } } $data = json_decode($_POST['json'] ?? '{}', true) ?: []; try { // ── Step 4: Sanitise and validate input ─────────────────────────────────── // Done before the transaction so validation errors don't acquire DB locks. $name = trim($data['name'] ?? ''); $surname = trim($data['surname'] ?? ''); $username = strtolower(trim($data['username'] ?? '')); $password = $data['password'] ?? ''; $confirm = $data['confirm_password'] ?? ''; if (!$name || !$surname || !$username || !$password || !$confirm) { throw new Exception('All fields are required.'); } // ── Step 5: Username format, length, and reserved names ────────────────── if (!preg_match('/^[a-z0-9_]{3,32}$/', $username)) { throw new Exception('Username must be 3–32 characters and may only contain lowercase letters, numbers and underscores.'); } $reserved = ['admin', 'owner', 'support', 'root', 'system', 'superuser', 'administrator']; if (in_array($username, $reserved, true)) { throw new Exception('That username is reserved. Please choose another.'); } // ── Step 6: Password match and strength ─────────────────────────────────── if ($password !== $confirm) { throw new Exception('Passwords do not match.'); } $pm = new PasswordManager($pdo1, $include_url); $result = $pm->checkStrength($password, [$name, $surname, $username]); if ($result['score'] < PasswordManager::MIN_SCORE) { $msg = $result['warning'] ?: ($result['suggestions'][0] ?? 'Please choose a stronger password.'); throw new Exception('Password is too weak. ' . $msg); } $hashed = password_hash($password, PASSWORD_BCRYPT); // ── Steps 3 + 7–9: Atomic token re-validation and activation ───────────── // SELECT FOR UPDATE locks the row so a concurrent resendInvite or removeUser // cannot mutate the token between our check and the UPDATE. $pdo1->beginTransaction(); $sth = $pdo1->prepare( "SELECT user_id FROM user WHERE user_id = :uid AND verify_token = :token AND status = 'pending' AND license = 'user' AND verify_expires_at > NOW() LIMIT 1 FOR UPDATE" ); $sth->execute([':uid' => $user_id, ':token' => $token]); if (!$sth->fetchColumn()) { $pdo1->rollBack(); $answer['message'] = 'Invitation has expired or already been used. Please request a new invitation.'; http_response_code(403); exit(json_encode($answer)); } // ── Step 7–8: Activate account ──────────────────────────────────────────── $stmt = $pdo1->prepare( "UPDATE user SET name = :name, surname = :surname, username = :username, password = :password, status = 'active', verify_token = NULL, verify_expires_at = NULL WHERE user_id = :uid" ); try { $stmt->execute([ ':name' => $name, ':surname' => $surname, ':username' => $username, ':password' => $hashed, ':uid' => $user_id, ]); } catch (PDOException $e) { $pdo1->rollBack(); // SQLSTATE 23000 = unique constraint violation (duplicate username) if ($e->getCode() === '23000') { throw new Exception('Username is already taken. Please choose another.'); } throw $e; } if ($stmt->rowCount() !== 1) { $pdo1->rollBack(); $answer['message'] = 'Invitation is no longer valid.'; http_response_code(403); exit(json_encode($answer)); } // ── Step 9: Clear invite token from company_map_user ───────────────────── $pdo1->prepare( "UPDATE company_map_user SET invite_token = NULL, invite_expires_at = NULL WHERE user_id = :uid" )->execute([':uid' => $user_id]); $pdo1->commit(); // ── Step 10: Clear session invite keys ──────────────────────────────────── unset($_SESSION['invited_user_id'], $_SESSION['invited_token']); $answer['success'] = 1; $answer['message'] = 'Account setup complete.'; } catch (Exception $e) { if ($pdo1->inTransaction()) $pdo1->rollBack(); $answer['message'] = $e->getMessage(); http_response_code(400); } exit(json_encode($answer));