264 lines
10 KiB
PHP
264 lines
10 KiB
PHP
<?php
|
||
require '../session.php';
|
||
require '../config.php';
|
||
require '../dbconn.php';
|
||
|
||
$token = trim($_GET['token'] ?? '');
|
||
|
||
if (!$token) {
|
||
header('Location: ' . $server_url . 'login/index.php');
|
||
exit;
|
||
}
|
||
|
||
// Reject if a user is already logged in — opening an invite link in an active
|
||
// session would bind a different account's identity into the current session.
|
||
if (!empty($_SESSION['login_company_id'])) {
|
||
require '../include_header.php';
|
||
?>
|
||
<body>
|
||
<div class="container py-5" style="max-width:480px;">
|
||
<div class="text-center mb-5">
|
||
<a href="<?php echo $server_url?>login/index.php" class="d-inline-block mb-4">
|
||
<img src="<?php echo $server_url?>assets/images/logo.svg" alt="">
|
||
</a>
|
||
</div>
|
||
<div class="card text-center">
|
||
<div class="card-body p-5">
|
||
<i class="ti ti-user-check text-warning mb-3" style="font-size:3rem;"></i>
|
||
<h2 class="fs-4 mb-2">Already Signed In</h2>
|
||
<p class="text-muted mb-4">You are already signed in. Please sign out first before accepting an invitation.</p>
|
||
<a href="<?php echo $server_url?>login/index.php" class="btn btn-primary">Go to Dashboard</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
<?php
|
||
exit;
|
||
}
|
||
|
||
// Validate token — must match a pending invited user (license='user') that has not expired
|
||
$sth = $pdo1->prepare(
|
||
"SELECT u.user_id, u.email, u.verify_expires_at, c.company_name
|
||
FROM user u
|
||
JOIN company_map_user m ON m.user_id = u.user_id
|
||
JOIN company_list c ON c.company_id = m.company_id
|
||
WHERE u.verify_token = :token
|
||
AND u.status = 'pending'
|
||
AND u.license = 'user'
|
||
LIMIT 1"
|
||
);
|
||
$sth->execute([':token' => $token]);
|
||
$row = $sth->fetch(PDO::FETCH_ASSOC);
|
||
|
||
// Determine error state: cancelled (no row) or expired (row found but past expiry)
|
||
$invite_error = null;
|
||
if (!$row) {
|
||
$invite_error = 'cancelled';
|
||
} elseif (strtotime($row['verify_expires_at']) <= time()) {
|
||
$invite_error = 'expired';
|
||
}
|
||
|
||
if ($invite_error) {
|
||
require '../include_header.php';
|
||
$msg = $invite_error === 'expired'
|
||
? ['icon' => 'ti-clock-x', 'title' => 'Invitation Expired',
|
||
'body' => 'This invitation link has expired. Please contact your administrator to send a new invitation.']
|
||
: ['icon' => 'ti-user-x', 'title' => 'Invitation Cancelled',
|
||
'body' => 'This invitation has been cancelled. Please contact your administrator if you believe this is a mistake.'];
|
||
?>
|
||
<body>
|
||
<div class="container py-5" style="max-width:480px;">
|
||
<div class="text-center mb-5">
|
||
<a href="<?php echo $server_url?>login/index.php" class="d-inline-block mb-4">
|
||
<img src="<?php echo $server_url?>assets/images/logo.svg" alt="">
|
||
</a>
|
||
</div>
|
||
<div class="card text-center">
|
||
<div class="card-body p-5">
|
||
<i class="ti <?php echo $msg['icon']; ?> text-danger mb-3" style="font-size:3rem;"></i>
|
||
<h2 class="fs-4 mb-2"><?php echo $msg['title']; ?></h2>
|
||
<p class="text-muted mb-4"><?php echo $msg['body']; ?></p>
|
||
<a href="<?php echo $server_url?>login/index.php" class="btn btn-primary">
|
||
Back to Sign In
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
<?php
|
||
exit;
|
||
}
|
||
|
||
// Regenerate session ID before binding invite identity to prevent session fixation
|
||
session_regenerate_id(true);
|
||
|
||
$_SESSION['invited_user_id'] = (int)$row['user_id'];
|
||
$_SESSION['invited_token'] = $token;
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||
|
||
$company_name = htmlspecialchars($row['company_name']);
|
||
$invite_email = htmlspecialchars($row['email']);
|
||
|
||
require '../include_header.php';
|
||
?>
|
||
|
||
<body>
|
||
|
||
<div class="container py-5" style="max-width:520px;">
|
||
|
||
<div class="text-center mb-5">
|
||
<a href="<?php echo $server_url?>login/index.php" class="d-inline-block mb-4">
|
||
<img src="<?php echo $server_url?>assets/images/logo.svg" alt="">
|
||
</a>
|
||
<h1 class="h4 mb-1">You've been invited!</h1>
|
||
<p class="text-muted">Complete your account setup to join <strong><?php echo $company_name ?></strong>.</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-body p-5">
|
||
<h2 class="fs-5 mb-1"><i class="ti ti-user-check me-2"></i>Account Setup</h2>
|
||
<p class="text-muted small mb-4">Your email: <strong><?php echo $invite_email ?></strong></p>
|
||
|
||
<div class="row g-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label">First Name <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="name" placeholder="First name">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label">Last Name <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="surname" placeholder="Last name">
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">Username <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control" id="username" placeholder="Lowercase letters, numbers, underscores" minlength="3" maxlength="32">
|
||
<div class="form-text">3–32 characters. Used to log in. Cannot be changed later.</div>
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">Password <span class="text-danger">*</span></label>
|
||
<div class="input-group">
|
||
<input type="password" class="form-control" id="password" placeholder="Choose a strong password"
|
||
oninput="on_password_input(this.value)">
|
||
<button class="btn btn-outline-secondary toggle-pw" type="button" data-target="password">
|
||
<i class="ti ti-eye"></i>
|
||
</button>
|
||
</div>
|
||
<div class="mt-2">
|
||
<div class="progress" style="height:5px;">
|
||
<div id="pw_strength_bar" class="progress-bar"
|
||
style="width:0%;transition:width .25s,background-color .25s;border-radius:4px;"></div>
|
||
</div>
|
||
<div class="d-flex justify-content-between align-items-start mt-1 gap-2">
|
||
<small id="pw_strength_label" class="fw-semibold" style="white-space:nowrap;">—</small>
|
||
<small id="pw_feedback" class="text-muted text-end"></small>
|
||
</div>
|
||
</div>
|
||
<div class="form-text">Minimum required strength: <strong>Strong (3/4)</strong></div>
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label">Confirm Password <span class="text-danger">*</span></label>
|
||
<div class="input-group">
|
||
<input type="password" class="form-control" id="confirm_password" placeholder="Repeat your password">
|
||
<button class="btn btn-outline-secondary toggle-pw" type="button" data-target="confirm_password">
|
||
<i class="ti ti-eye"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-flex justify-content-end mt-4">
|
||
<button class="btn btn-primary px-5" id="btn_finish" onclick="finish_setup()">
|
||
<i class="ti ti-rocket me-1"></i>Complete Setup
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/zxcvbn@4.4.2/dist/zxcvbn.js"></script>
|
||
<script>
|
||
|
||
const STRENGTH_LEVELS = [
|
||
{ label: 'Very weak', color: '#dc3545', pct: 20 },
|
||
{ label: 'Weak', color: '#fd7e14', pct: 40 },
|
||
{ label: 'Fair', color: '#ffc107', pct: 60 },
|
||
{ label: 'Strong', color: '#198754', pct: 80 },
|
||
{ label: 'Very strong', color: '#0d6efd', pct: 100 },
|
||
];
|
||
|
||
var pw_score = -1;
|
||
|
||
function on_password_input(pw) {
|
||
if (!pw) {
|
||
pw_score = -1;
|
||
$('#pw_strength_bar').css({ width: '0%', backgroundColor: '' });
|
||
$('#pw_strength_label').text('—').css('color', '');
|
||
$('#pw_feedback').text('');
|
||
return;
|
||
}
|
||
const user_inputs = [$('#name').val(), $('#surname').val(), $('#username').val()].filter(Boolean);
|
||
const result = zxcvbn(pw, user_inputs);
|
||
pw_score = result.score;
|
||
const lvl = STRENGTH_LEVELS[pw_score];
|
||
$('#pw_strength_bar').css({ width: lvl.pct + '%', backgroundColor: lvl.color });
|
||
$('#pw_strength_label').text(lvl.label).css('color', lvl.color);
|
||
$('#pw_feedback').text(result.feedback.warning || result.feedback.suggestions[0] || '');
|
||
}
|
||
|
||
$(function () {
|
||
$(document).on('click', '.toggle-pw', function () {
|
||
const $input = $('#' + $(this).data('target'));
|
||
const isText = $input.attr('type') === 'text';
|
||
$input.attr('type', isText ? 'password' : 'text');
|
||
$(this).find('i').toggleClass('ti-eye ti-eye-off');
|
||
});
|
||
});
|
||
|
||
function finish_setup() {
|
||
const name = $('#name').val().trim();
|
||
const surname = $('#surname').val().trim();
|
||
const username = $('#username').val().trim();
|
||
const password = $('#password').val();
|
||
const confirm = $('#confirm_password').val();
|
||
|
||
if (!name || !surname || !username || !password || !confirm) {
|
||
bootbox.alert('All fields are required.');
|
||
return;
|
||
}
|
||
|
||
if (pw_score < 3) {
|
||
bootbox.alert('Password is too weak. Please choose a stronger password (Strong or above).');
|
||
return;
|
||
}
|
||
|
||
if (password !== confirm) {
|
||
bootbox.alert('Passwords do not match.');
|
||
return;
|
||
}
|
||
|
||
const $btn = $('#btn_finish');
|
||
$btn.prop('disabled', true).html('<i class="ti ti-loader-2 me-1"></i>Setting up…');
|
||
|
||
ajax_request({
|
||
url: '<?php echo $server_url?>login/api/engine/invited_onboarding.php',
|
||
autoPrepare: false,
|
||
data: { json: JSON.stringify({ name, surname, username, password, confirm_password: confirm }) },
|
||
onSuccess: function () {
|
||
bootbox.alert('Account setup complete! Please sign in.', function () {
|
||
window.location.href = '<?php echo $server_url?>login/index.php';
|
||
});
|
||
},
|
||
onError: function () {
|
||
$btn.prop('disabled', false).html('<i class="ti ti-rocket me-1"></i>Complete Setup');
|
||
},
|
||
});
|
||
}
|
||
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|