- Upgraded all plain `require` to `require_once` across 172 api/engine and api/engine_report files to prevent class-redeclaration errors - Added issue button, issue_invoice() with GL toastr, and delete_invoice() to expense/manage_purchase_invoice.php, bringing it in line with po/manage_purchase_invoice.php - Added can_delete role guard (admin/owner only) to trash icons on revenue/invoice.php and expense/purchase_invoice.php, matching the existing pattern in finance/receipt.php and finance/payment.php Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
150 lines
6.3 KiB
PHP
150 lines
6.3 KiB
PHP
<?php
|
||
/**
|
||
* invited_onboarding.php — Complete account setup for an invited user.
|
||
*
|
||
* Called by: invited_onboarding.php page AJAX after the user fills in
|
||
* their name, username, and password.
|
||
*
|
||
* The invited user was created with license='user', status='pending', and
|
||
* default_company = the inviting company. This endpoint activates the account
|
||
* so they can log in using the inviting company's SMTP for OTP delivery.
|
||
*
|
||
* Full flow:
|
||
* 1. Session guard — rejects if 'invited_user_id' is missing.
|
||
* 2. CSRF check.
|
||
* 3. Re-validate token against DB (expiry + status='pending' + license='user').
|
||
* 4. Validate and sanitise input fields.
|
||
* 5. Username format and uniqueness check.
|
||
* 6. Password match and strength check.
|
||
* 7. Hash password.
|
||
* 8. UPDATE user: name, surname, username, password, status='active',
|
||
* verify_token=NULL, verify_expires_at=NULL.
|
||
* 9. UPDATE company_map_user: invite_token=NULL.
|
||
* 10. 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';
|
||
require_once '../../../assets/utils/classes/PasswordManager.php';
|
||
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
|
||
$answer = ['success' => 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) || $csrf !== ($_SESSION['csrf_token'] ?? '')) {
|
||
http_response_code(403);
|
||
$answer['message'] = 'Invalid request.';
|
||
exit(json_encode($answer));
|
||
}
|
||
}
|
||
|
||
$data = json_decode($_POST['json'] ?? '{}', true) ?: [];
|
||
|
||
try {
|
||
|
||
// ── Step 3: Re-validate token ─────────────────────────────────────────────
|
||
$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"
|
||
);
|
||
$sth->execute([':uid' => $user_id, ':token' => $token]);
|
||
if (!$sth->fetchColumn()) {
|
||
$answer['message'] = 'Invitation has expired or already been used. Please request a new invitation.';
|
||
http_response_code(403);
|
||
exit(json_encode($answer));
|
||
}
|
||
|
||
// ── Step 4: Sanitise and validate input ───────────────────────────────────
|
||
$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 and uniqueness ────────────────────────────────
|
||
if (!preg_match('/^[a-z0-9_]+$/', $username)) {
|
||
throw new Exception('Username may only contain lowercase letters, numbers and underscores.');
|
||
}
|
||
|
||
$sth = $pdo1->prepare("SELECT user_id FROM user WHERE username = :u AND user_id != :uid LIMIT 1");
|
||
$sth->execute([':u' => $username, ':uid' => $user_id]);
|
||
if ($sth->fetchColumn()) {
|
||
throw new Exception('Username is already taken. 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);
|
||
}
|
||
|
||
// ── Step 7–8: Hash and activate account ──────────────────────────────────
|
||
$hashed = password_hash($password, PASSWORD_BCRYPT);
|
||
|
||
$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"
|
||
)->execute([
|
||
':name' => $name,
|
||
':surname' => $surname,
|
||
':username' => $username,
|
||
':password' => $hashed,
|
||
':uid' => $user_id,
|
||
]);
|
||
|
||
// ── Step 9: Clear invite token from company_map_user ─────────────────────
|
||
$pdo1->prepare(
|
||
"UPDATE company_map_user SET invite_token = NULL WHERE user_id = :uid"
|
||
)->execute([':uid' => $user_id]);
|
||
|
||
// ── 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) {
|
||
$answer['message'] = $e->getMessage();
|
||
http_response_code(400);
|
||
}
|
||
|
||
exit(json_encode($answer));
|