Files
wms-app/app/login/invited_onboarding.php
T
Thanakorn f11af6e949 Self-host front-end libraries, minimal sign-in header and CSP
- sign-in pages load only what they use (include_login_header.php, ajax_core.js)
- every library, font and data file served from assets/vendor with pinned
  versions (VERSIONS.json); no CDN at runtime
- Content-Security-Policy on app, sign-in and landing pages
- remove httpbin Dropzone target and source-map references
2026-09-24 14:53:41 +07:00

264 lines
10 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
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 __DIR__ . '/include_login_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 __DIR__ . '/include_login_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 __DIR__ . '/include_login_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="<?php echo $server_url?>assets/vendor/zxcvbn/4.4.2/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>