Files
wms-app/app/login/verify.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

74 lines
3.0 KiB
PHP

<?php
require '../session.php';
require '../config.php';
require '../dbconn.php';
require '../assets/utils/db_helpers.php';
$answer = ['success' => 0, 'message' => ''];
$token = trim($_GET['token'] ?? '');
if (!$token) {
header('Location: ' . $server_url . 'login/index.php');
exit;
}
// ── Look up token ─────────────────────────────────────────────
// license='owner' guard: invited users also have verify_token set, but they
// must use invited_onboarding.php — never this flow.
$sth = $pdo1->prepare("
SELECT user_id, name, status, verify_expires_at
FROM user
WHERE verify_token = :token
AND license = 'owner'
LIMIT 1
");
$sth->execute([':token' => $token]);
$user = $sth->fetch(PDO::FETCH_ASSOC);
// ── Invalid token ─────────────────────────────────────────────
if (!$user) {
$_SESSION['verify_error'] = 'This verification link is invalid or has already been used.';
header('Location: ' . $server_url . 'login/index.php');
exit;
}
// ── Already verified — check if onboarding still needed ───────
if ($user['status'] === 'active') {
$sth = $pdo1->prepare("
SELECT default_company FROM user WHERE user_id = :id LIMIT 1
");
$sth->execute([':id' => $user['user_id']]);
$default_company = $sth->fetchColumn();
if (empty($default_company)) {
// Verified but never completed onboarding — resume it
$_SESSION['onboarding_user_id'] = (int)$user['user_id'];
$_SESSION['onboarding_name'] = $user['name'];
session_write_close();
header('Location: ' . $server_url . 'login/onboarding.php');
} else {
// Fully set up — just go to login
header('Location: ' . $server_url . 'login/index.php');
}
exit;
}
// ── Expired ───────────────────────────────────────────────────
if (strtotime($user['verify_expires_at']) < time()) {
// Delete the expired pending account
$sth = $pdo1->prepare("DELETE FROM user WHERE user_id = :id AND status = 'pending'");
$sth->execute([':id' => $user['user_id']]);
$_SESSION['verify_error'] = 'This verification link has expired. Please register again.';
header('Location: ' . $server_url . 'login/index.php');
exit;
}
// ── Store user_id in session for onboarding ───────────────────
$_SESSION['onboarding_user_id'] = (int)$user['user_id'];
$_SESSION['onboarding_name'] = $user['name'];
session_write_close();
header('Location: ' . $server_url . 'login/onboarding.php');
exit;?>