add forget password

This commit is contained in:
Thanakorn S
2026-05-12 17:19:22 +07:00
parent 8f1c5c49fd
commit 79e66bd354
5 changed files with 286 additions and 2 deletions
+2 -2
View File
@@ -349,7 +349,7 @@ if (password_verify(trim($data["password"]), $temp["password"])) {
// don't count so a typo in your own name doesn't eat your own attempts.
if ($user_id) {
$attempts = (int)($temp['login_attempts'] ?? 0) + 1;
if ($attempts >= 10) {
if ($attempts >= 5) {
$locked_until = date('Y-m-d H:i:s', strtotime('+30 minutes'));
$pdo1->prepare("UPDATE user SET login_attempts = :a, locked_until = :l WHERE user_id = :id")
->execute([':a' => $attempts, ':l' => $locked_until, ':id' => $user_id]);
@@ -361,7 +361,7 @@ if (password_verify(trim($data["password"]), $temp["password"])) {
$answer['message'] = "Incorrect Password";
}
} else {
$answer['message'] = "Incorrect Password";
$answer['message'] = "Incorrect Username";
}
setcookie("u", "", time() - 1, "/");
@@ -0,0 +1,32 @@
<?php
require '../../../session.php';
define('UNAUTHENTICATED_ROUTE', true);
require '../../../assets/utils/db_auth.php';
// Resolve user by username or email
$identifier = strtolower(trim($data['identifier'] ?? ''));
if (!$identifier) {
http_response_code(422);
exit(json_encode(['success' => 0, 'message' => 'Please enter your username or email.']));
}
$sth = $pdo1->prepare(
"SELECT user_id, default_company FROM user WHERE username = :i OR email = :i LIMIT 1"
);
$sth->execute([':i' => $identifier]);
$user = $sth->fetch(PDO::FETCH_ASSOC);
if (!$user) {
http_response_code(404);
exit(json_encode(['success' => 0, 'message' => 'No account found with that username or email.']));
}
$user_id = (int)$user['user_id'];
$company_id = (int)($user['default_company'] ?? 0);
require_once $include_url . 'assets/utils/classes/PasswordResetManager.php';
$manager = new PasswordResetManager($pdo1, $pdo2, $include_url, $SMTP, $pinkey);
$manager->handleRequestOtp($user_id, $company_id);
@@ -0,0 +1,17 @@
<?php
require '../../../session.php';
define('UNAUTHENTICATED_ROUTE', true);
require '../../../assets/utils/db_auth.php';
if (empty($_SESSION['reset_user_id'])) {
http_response_code(400);
exit(json_encode(['success' => 0, 'message' => 'No active reset request. Please request a new OTP.']));
}
$user_id = (int)$_SESSION['reset_user_id'];
require_once $include_url . 'assets/utils/classes/PasswordResetManager.php';
$manager = new PasswordResetManager($pdo1, $pdo2, $include_url, $SMTP, $pinkey);
$manager->handleConfirmReset($user_id, $data);
+234
View File
@@ -0,0 +1,234 @@
<?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 '../include_header.php';
?>
<body>
<script src="https://cdn.jsdelivr.net/npm/zxcvbn@4.4.2/dist/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/favicon.png" alt="" width="40">
</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>
OTP sent to <strong id="masked_email"></strong>
— 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) {
$('#masked_email').text(r.masked_email);
$('#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>
+1
View File
@@ -43,6 +43,7 @@
<div class="mb-3">
<label for="password" class="form-label d-flex justify-content-between">
<span>Password</span>
<a href="<?php echo $server_url?>login/forgot_password.php" class="small link-primary">Forgot password?</a>
</label>
<input id="password" type="password" class="form-control" placeholder="Password" required minlength="6">
<div class="invalid-feedback">Please provide a password (min 6 characters).</div>