- OTP attempt limits, constant-time compare, random reference codes - DB-backed rate limits (429) on sign-in, OTP, reset, register, onboarding - one generic sign-in failure message; reset request no longer reveals accounts - no password kept in the session; real status codes on failures
237 lines
8.5 KiB
PHP
237 lines
8.5 KiB
PHP
<?php
|
|
require '../session.php';
|
|
require '../config.php';
|
|
|
|
if (!empty($_SESSION['login_status'])) {
|
|
header('Location: ' . $server_url . 'dashboard/index.php');
|
|
exit;
|
|
}
|
|
|
|
if (empty($_SESSION['csrf_token'])) {
|
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
}
|
|
|
|
require __DIR__ . '/include_login_header.php';
|
|
?>
|
|
|
|
<body>
|
|
|
|
<script src="<?php echo $server_url?>assets/vendor/zxcvbn/4.4.2/zxcvbn.js"></script>
|
|
|
|
<div class="container d-flex align-items-center justify-content-center min-vh-100">
|
|
<div class="card" style="max-width:420px; width:100%;">
|
|
<div class="card-body p-5">
|
|
|
|
<div class="text-center mb-4">
|
|
<a href="<?php echo $server_url?>login/index.php" class="mb-4 d-inline-block">
|
|
<img src="<?php echo $server_url?>assets/images/logo.svg" alt="">
|
|
</a>
|
|
<h1 class="h5 mb-1">Reset your password</h1>
|
|
<p class="text-muted small mb-0" id="subtitle">Enter your username or email to receive an OTP.</p>
|
|
</div>
|
|
|
|
<!-- Step 1: request OTP -->
|
|
<div id="step_request">
|
|
<div class="mb-3">
|
|
<label class="form-label">Username or Email</label>
|
|
<input type="text" class="form-control" id="identifier"
|
|
placeholder="Enter your username or email" autofocus>
|
|
</div>
|
|
<button class="btn btn-primary w-100 mb-3" id="btn_request" onclick="request_otp()">
|
|
<i class="ti ti-send me-1"></i>Send OTP
|
|
</button>
|
|
<p class="text-center text-muted small mb-0">
|
|
Remember your password?
|
|
<a href="<?php echo $server_url?>login/index.php" class="link-primary">Sign in</a>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Step 2: enter OTP + new password (hidden until step 1 succeeds) -->
|
|
<div id="step_reset" class="d-none">
|
|
<div class="alert alert-info small py-2 mb-4">
|
|
<i class="ti ti-mail me-1"></i>
|
|
<span id="request_message"></span>
|
|
Reference <strong id="ref_code"></strong>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">OTP <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="otp"
|
|
placeholder="6-digit OTP" maxlength="6" autocomplete="one-time-code">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">New Password <span class="text-danger">*</span></label>
|
|
<div class="input-group">
|
|
<input type="password" class="form-control" id="new_password" placeholder="New password"
|
|
oninput="on_password_input(this.value)">
|
|
<button class="btn btn-outline-secondary toggle-pw" type="button" data-target="new_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 mt-1">
|
|
<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>
|
|
<div class="mb-4">
|
|
<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 new password">
|
|
<button class="btn btn-outline-secondary toggle-pw" type="button" data-target="confirm_password">
|
|
<i class="ti ti-eye"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-primary w-100 mb-3" id="btn_reset" onclick="reset_password()">
|
|
<i class="ti ti-lock-check me-1"></i>Reset Password
|
|
</button>
|
|
<p class="text-center">
|
|
<a href="javascript:;" class="small link-secondary" onclick="go_back()">
|
|
<i class="ti ti-arrow-left me-1"></i>Use a different account
|
|
</a>
|
|
</p>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<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 result = zxcvbn(pw);
|
|
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');
|
|
});
|
|
|
|
$('#identifier').on('keydown', function (e) {
|
|
if (e.key === 'Enter') request_otp();
|
|
});
|
|
$('#otp, #confirm_password').on('keydown', function (e) {
|
|
if (e.key === 'Enter') reset_password();
|
|
});
|
|
});
|
|
|
|
|
|
function request_otp() {
|
|
const identifier = $('#identifier').val().trim();
|
|
if (!identifier) {
|
|
bootbox.alert('Please enter your username or email.');
|
|
return;
|
|
}
|
|
|
|
const $btn = $('#btn_request');
|
|
$btn.prop('disabled', true).html('<i class="ti ti-loader-2 me-1"></i>Sending…');
|
|
|
|
ajax_request({
|
|
url: '<?php echo $server_url?>login/api/engine/request_reset_otp.php',
|
|
autoPrepare: false,
|
|
data: { json: JSON.stringify({ action: 'read', identifier: identifier }) },
|
|
onSuccess: function (r) {
|
|
// The server answers the same way whether or not the account exists,
|
|
// so there is no masked email to show — only its generic message.
|
|
$('#request_message').text(r.message || '');
|
|
$('#ref_code').text(r.reference || '');
|
|
$('#step_request').addClass('d-none');
|
|
$('#step_reset').removeClass('d-none');
|
|
$('#subtitle').text('Enter the OTP from your email and choose a new password.');
|
|
$('#otp').focus();
|
|
},
|
|
onError: function () {
|
|
$btn.prop('disabled', false).html('<i class="ti ti-send me-1"></i>Send OTP');
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
function reset_password() {
|
|
const otp = $('#otp').val().trim();
|
|
const np = $('#new_password').val();
|
|
const cp = $('#confirm_password').val();
|
|
|
|
if (!otp || !np || !cp) {
|
|
bootbox.alert('Please fill in all fields.');
|
|
return;
|
|
}
|
|
if (np !== cp) {
|
|
bootbox.alert('Passwords do not match.');
|
|
return;
|
|
}
|
|
if (pw_score < 3) {
|
|
bootbox.alert('Please choose a stronger password.');
|
|
return;
|
|
}
|
|
|
|
const $btn = $('#btn_reset');
|
|
$btn.prop('disabled', true).html('<i class="ti ti-loader-2 me-1"></i>Resetting…');
|
|
|
|
ajax_request({
|
|
url: '<?php echo $server_url?>login/api/engine/reset_password_otp.php',
|
|
autoPrepare: false,
|
|
data: { json: JSON.stringify({
|
|
action: 'update',
|
|
otp: otp,
|
|
new_password: np,
|
|
confirm_password: cp,
|
|
})},
|
|
onSuccess: function (r) {
|
|
bootbox.alert('Password reset successfully. Please sign in with your new password.', function () {
|
|
window.location.href = '<?php echo $server_url?>login/index.php';
|
|
});
|
|
},
|
|
onError: function () {
|
|
$btn.prop('disabled', false).html('<i class="ti ti-lock-check me-1"></i>Reset Password');
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
function go_back() {
|
|
$('#step_reset').addClass('d-none');
|
|
$('#step_request').removeClass('d-none');
|
|
$('#subtitle').text('Enter your username or email to receive an OTP.');
|
|
$('#btn_request').prop('disabled', false).html('<i class="ti ti-send me-1"></i>Send OTP');
|
|
$('#identifier').val('').focus();
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
</html>
|