665 lines
28 KiB
PHP
665 lines
28 KiB
PHP
<?php
|
|
session_start();
|
|
require '../config.php';
|
|
require '../include_header.php';
|
|
?>
|
|
|
|
<body>
|
|
<?php require '../include_topbar.php'; ?>
|
|
<?php require '../include_setting_sidebar.php'; ?>
|
|
|
|
<main id="content" class="content py-15">
|
|
<div class="container-fluid">
|
|
|
|
<!-- Page header -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-6 gap-3">
|
|
<div>
|
|
<h1 class="fs-3 mb-1">Profile</h1>
|
|
<p class="mb-0">Manage your account details</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-5">
|
|
|
|
<!-- LEFT: Avatar card -->
|
|
<div class="col-lg-3 col-12">
|
|
<div class="card">
|
|
<div class="card-body p-6 text-center">
|
|
<div class="position-relative d-inline-block mb-4">
|
|
<img id="avatar_preview"
|
|
src="<?php echo $server_url?>assets/images/avatar-1.jpg"
|
|
alt="Profile picture"
|
|
class="rounded-circle border"
|
|
style="width:96px;height:96px;object-fit:cover;">
|
|
<label for="profile_picture"
|
|
class="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle p-0 d-flex align-items-center justify-content-center"
|
|
style="width:26px;height:26px;cursor:pointer;" title="Change photo">
|
|
<i class="ti ti-camera" style="font-size:12px;line-height:1;"></i>
|
|
</label>
|
|
<input type="file" id="profile_picture" accept="image/*" class="d-none">
|
|
</div>
|
|
<h5 class="mb-0" id="display_name">—</h5>
|
|
<p class="text-muted small mb-0" id="display_username">—</p>
|
|
<p class="text-muted small mt-1 mb-0" id="display_email">—</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RIGHT -->
|
|
<div class="col-lg-9 col-12">
|
|
|
|
<!-- Personal information -->
|
|
<div class="card mb-5" id="profileForm">
|
|
<div class="card-body p-6">
|
|
<h2 class="fs-5 mb-4">Personal Information</h2>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="name" class="form-label">First Name <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="name" placeholder="First name" required>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="surname" class="form-label">Last Name <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="surname" placeholder="Last name" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="email" class="form-label">Email <span class="text-danger">*</span></label>
|
|
<input type="email" class="form-control" id="email" placeholder="your@email.com" required>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="phone" class="form-label">Phone</label>
|
|
<input type="text" class="form-control" id="phone" placeholder="e.g. 0812345678">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="country" class="form-label">Country</label>
|
|
<select class="form-select" id="country">
|
|
<option value="">— Select country —</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Username</label>
|
|
<input type="text" class="form-control" id="username_display" disabled>
|
|
<div class="form-text">Username cannot be changed</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-12 mb-3">
|
|
<label for="address" class="form-label">Address</label>
|
|
<textarea class="form-control" id="address" rows="2" placeholder="Enter your address"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2 mt-2">
|
|
<button class="btn btn-primary" onclick="save_profile();">
|
|
<i class="ti ti-device-floppy me-1"></i>Save Changes
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Change password -->
|
|
<div class="card" id="passwordForm">
|
|
<div class="card-body p-6">
|
|
<h2 class="fs-5 mb-1">Change Password</h2>
|
|
<p class="text-muted small mb-4">Minimum required strength: <strong>Strong (3/4)</strong></p>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="current_password" class="form-label">
|
|
Current Password <span class="text-danger">*</span>
|
|
</label>
|
|
<div class="input-group">
|
|
<input type="password" class="form-control" id="current_password"
|
|
placeholder="Enter current password" oninput="update_pw_button()">
|
|
<button class="btn btn-outline-secondary toggle-pw" type="button" data-target="current_password">
|
|
<i class="ti ti-eye"></i>
|
|
</button>
|
|
</div>
|
|
<div class="form-text">
|
|
<a href="javascript:;" class="link-warning" onclick="open_forgot_modal();">
|
|
<i class="ti ti-help-circle me-1"></i>Forgot your current password?
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="new_password" 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="Enter new password" oninput="on_new_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 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>
|
|
|
|
<div class="col-md-6 mb-3">
|
|
<label for="confirm_password" 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" oninput="check_confirm_match()">
|
|
<button class="btn btn-outline-secondary toggle-pw" type="button" data-target="confirm_password">
|
|
<i class="ti ti-eye"></i>
|
|
</button>
|
|
</div>
|
|
<small id="pw_match_label" class="mt-1 d-block"></small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2 mt-2">
|
|
<button class="btn btn-warning" id="btn_change_password"
|
|
onclick="change_password();" disabled>
|
|
<i class="ti ti-key me-1"></i>Update Password
|
|
</button>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /RIGHT -->
|
|
</div><!-- /row -->
|
|
|
|
</div>
|
|
<?php require '../include_ending.php'; ?>
|
|
</main>
|
|
|
|
|
|
<!-- ═══════════════════════════════════════════════
|
|
FORGOT PASSWORD MODAL
|
|
Step 1 → confirm send OTP
|
|
Step 2 → enter OTP + new password
|
|
════════════════════════════════════════════════════ -->
|
|
<div class="modal fade" id="forgotModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
|
|
<div class="modal-header border-0 pb-0">
|
|
<h5 class="modal-title">
|
|
<i class="ti ti-lock-open me-2 text-warning"></i>Reset Password
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"
|
|
onclick="reset_forgot_modal();"></button>
|
|
</div>
|
|
|
|
<!-- Step 1 -->
|
|
<div id="forgot_step1">
|
|
<div class="modal-body pt-3">
|
|
<p class="text-muted small mb-3">
|
|
An OTP will be sent to your registered email address.
|
|
</p>
|
|
<div class="alert alert-secondary py-2 px-3 small mb-0">
|
|
<i class="ti ti-mail me-1"></i>
|
|
<span id="modal_masked_email">—</span>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-secondary btn-sm"
|
|
data-bs-dismiss="modal" onclick="reset_forgot_modal();">Cancel</button>
|
|
<button type="button" class="btn btn-warning btn-sm" id="btn_send_otp"
|
|
onclick="send_reset_otp();">
|
|
<i class="ti ti-send me-1"></i>Send OTP
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2 -->
|
|
<div id="forgot_step2" class="d-none">
|
|
<div class="modal-body pt-3">
|
|
|
|
<p class="text-muted small mb-3">
|
|
OTP sent to <strong id="modal_masked_email2"></strong> —
|
|
Reference: <strong id="modal_reference"></strong>
|
|
</p>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">OTP <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="reset_otp"
|
|
placeholder="6-digit OTP" maxlength="6"
|
|
oninput="this.value=this.value.replace(/\D/g,''); update_reset_button();">
|
|
<div class="form-text">
|
|
Valid for 5 minutes.
|
|
<a href="javascript:;" class="link-primary" onclick="send_reset_otp();">Resend OTP</a>
|
|
</div>
|
|
</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="reset_new_password"
|
|
placeholder="Enter new password"
|
|
oninput="on_reset_password_input(this.value)">
|
|
<button class="btn btn-outline-secondary toggle-pw" type="button"
|
|
data-target="reset_new_password">
|
|
<i class="ti ti-eye"></i>
|
|
</button>
|
|
</div>
|
|
<div class="mt-2">
|
|
<div class="progress" style="height:5px;">
|
|
<div id="reset_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 gap-2">
|
|
<small id="reset_strength_label" class="fw-semibold" style="white-space:nowrap;">—</small>
|
|
<small id="reset_feedback" class="text-muted text-end"></small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-0">
|
|
<label class="form-label">Confirm Password <span class="text-danger">*</span></label>
|
|
<div class="input-group">
|
|
<input type="password" class="form-control" id="reset_confirm_password"
|
|
placeholder="Repeat new password"
|
|
oninput="check_reset_confirm_match()">
|
|
<button class="btn btn-outline-secondary toggle-pw" type="button"
|
|
data-target="reset_confirm_password">
|
|
<i class="ti ti-eye"></i>
|
|
</button>
|
|
</div>
|
|
<small id="reset_match_label" class="mt-1 d-block"></small>
|
|
</div>
|
|
|
|
</div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-secondary btn-sm"
|
|
data-bs-dismiss="modal" onclick="reset_forgot_modal();">Cancel</button>
|
|
<button type="button" class="btn btn-primary btn-sm"
|
|
id="btn_reset_confirm" onclick="confirm_reset_password();" disabled>
|
|
<i class="ti ti-lock-check me-1"></i>Reset Password
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<script>
|
|
// ═══════════════════════════════════════════════════════
|
|
// Shared strength config
|
|
// ═══════════════════════════════════════════════════════
|
|
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 },
|
|
];
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// State
|
|
// ═══════════════════════════════════════════════════════
|
|
var pw_score = -1;
|
|
var pw_debounce = null;
|
|
var pw_check_xhr = null;
|
|
var reset_pw_score = -1;
|
|
var reset_pw_debounce = null;
|
|
var reset_pw_check_xhr = null;
|
|
var user_email = ''; // stored on retrieve, used for modal masking
|
|
|
|
function mask_email(email) {
|
|
const at = email.indexOf('@');
|
|
if (at < 0) return email;
|
|
return email.substring(0, 2)
|
|
+ '*'.repeat(Math.max(1, at - 2))
|
|
+ email.substring(at);
|
|
}
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// On load
|
|
// ═══════════════════════════════════════════════════════
|
|
$(async function () {
|
|
try {
|
|
await load_countries();
|
|
await retrieve_profile();
|
|
} catch(e) { console.error(e); }
|
|
});
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// COUNTRIES — load from ISO 3166 CDN
|
|
// ═══════════════════════════════════════════════════════
|
|
function load_countries() {
|
|
return $.getJSON(
|
|
'https://cdn.jsdelivr.net/npm/world_countries_lists@latest/data/countries/en/countries.json'
|
|
).then(function(data) {
|
|
const $sel = $('#country');
|
|
// Thailand first for convenience, then alphabetical
|
|
const sorted = data.slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
const th = sorted.findIndex(c => c.alpha2 === 'th');
|
|
if (th > -1) {
|
|
const [thailand] = sorted.splice(th, 1);
|
|
sorted.unshift(thailand);
|
|
}
|
|
sorted.forEach(function(c) {
|
|
$sel.append(new Option(c.name, c.name));
|
|
});
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// PROFILE — retrieve & save
|
|
// ═══════════════════════════════════════════════════════
|
|
function retrieve_profile() {
|
|
return ajax_request({
|
|
url: '<?php echo $server_url?>setting/api/engine/retrieve_profile.php',
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
onSuccess: function(res) {
|
|
const u = res.output;
|
|
$('#name').val(u.name);
|
|
$('#surname').val(u.surname);
|
|
$('#email').val(u.email);
|
|
$('#phone').val(u.phone);
|
|
$('#username_display').val(u.username);
|
|
$('#country').val(u.country); // select already populated by load_countries()
|
|
$('#address').val(u.address);
|
|
if (u.profile_picture) {
|
|
$('#avatar_preview').attr('src',
|
|
'<?php echo $server_url?>uploads/profile/' + u.profile_picture);
|
|
}
|
|
user_email = u.email;
|
|
refresh_display_panel(u.name, u.surname, u.username, u.email);
|
|
}
|
|
});
|
|
}
|
|
|
|
function save_profile() {
|
|
const fd = new FormData();
|
|
const file = $('#profile_picture')[0].files[0];
|
|
if (file) fd.append('profile_picture', file);
|
|
|
|
return ajax_request({
|
|
url: '<?php echo $server_url?>setting/api/engine/manage_profile.php',
|
|
autoPrepare: true,
|
|
checkRequired: 1,
|
|
action: 'update',
|
|
formData: fd,
|
|
onSuccess: function(res) {
|
|
if (res.profile_picture) {
|
|
$('#avatar_preview').attr('src',
|
|
'<?php echo $server_url?>uploads/profile/' + res.profile_picture);
|
|
}
|
|
refresh_display_panel(
|
|
$('#name').val(), $('#surname').val(),
|
|
$('#username_display').val(), $('#email').val()
|
|
);
|
|
bootbox.alert('Profile updated successfully.');
|
|
}
|
|
});
|
|
}
|
|
|
|
function refresh_display_panel(name, surname, username, email) {
|
|
$('#display_name').text((name + ' ' + surname).trim() || '—');
|
|
$('#display_username').text('@' + (username || '—'));
|
|
$('#display_email').text(email || '—');
|
|
}
|
|
|
|
// Live update display panel while typing
|
|
$('#name, #surname').on('input', function() {
|
|
$('#display_name').text(($('#name').val() + ' ' + $('#surname').val()).trim() || '—');
|
|
});
|
|
$('#email').on('input', function() {
|
|
$('#display_email').text($(this).val() || '—');
|
|
});
|
|
|
|
// Avatar preview
|
|
$('#profile_picture').on('change', function() {
|
|
const file = this.files[0];
|
|
if (!file) return;
|
|
const reader = new FileReader();
|
|
reader.onload = e => $('#avatar_preview').attr('src', e.target.result);
|
|
reader.readAsDataURL(file);
|
|
});
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// CHANGE PASSWORD
|
|
// ═══════════════════════════════════════════════════════
|
|
function on_new_password_input(pw) {
|
|
clearTimeout(pw_debounce);
|
|
if (!pw) { reset_strength_ui('pw'); update_pw_button(); return; }
|
|
pw_debounce = setTimeout(() => check_strength(pw, 'pw'), 350);
|
|
}
|
|
|
|
function check_confirm_match() {
|
|
const np = $('#new_password').val();
|
|
const cp = $('#confirm_password').val();
|
|
if (!cp) { $('#pw_match_label').text(''); return; }
|
|
if (np === cp) {
|
|
$('#pw_match_label').html('<span style="color:#198754;">✓ Passwords match</span>');
|
|
} else {
|
|
$('#pw_match_label').html('<span style="color:#dc3545;">✗ Passwords do not match</span>');
|
|
}
|
|
update_pw_button();
|
|
}
|
|
|
|
function update_pw_button() {
|
|
const cur = $('#current_password').val();
|
|
const np = $('#new_password').val();
|
|
const cp = $('#confirm_password').val();
|
|
const ready = cur && np && cp && (np === cp) && pw_score >= 3;
|
|
$('#btn_change_password').prop('disabled', !ready);
|
|
}
|
|
|
|
function change_password() {
|
|
return ajax_request({
|
|
url: '<?php echo $server_url?>setting/api/engine/change_password.php',
|
|
autoPrepare: false,
|
|
data: { json: JSON.stringify({
|
|
otp: document.getElementById('session-context').dataset.otp,
|
|
company_id: document.getElementById('session-context').dataset.companyId,
|
|
action: 'update',
|
|
current_password: $('#current_password').val(),
|
|
new_password: $('#new_password').val(),
|
|
confirm_password: $('#confirm_password').val(),
|
|
})},
|
|
onSuccess: function() {
|
|
bootbox.alert(
|
|
'Password changed successfully. You will be logged out now.',
|
|
function() { window.location.href = '<?php echo $server_url?>index.php'; }
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// FORGOT PASSWORD MODAL
|
|
// ═══════════════════════════════════════════════════════
|
|
function open_forgot_modal() {
|
|
reset_forgot_modal();
|
|
$('#modal_masked_email').text(user_email ? mask_email(user_email) : '—');
|
|
$('#forgotModal').modal('show');
|
|
}
|
|
|
|
function reset_forgot_modal() {
|
|
// Reset steps
|
|
$('#forgot_step1').removeClass('d-none');
|
|
$('#forgot_step2').addClass('d-none');
|
|
// Clear fields
|
|
$('#reset_otp').val('');
|
|
$('#reset_new_password').val('');
|
|
$('#reset_confirm_password').val('');
|
|
$('#reset_match_label').text('');
|
|
reset_strength_ui('reset');
|
|
update_reset_button();
|
|
}
|
|
|
|
function send_reset_otp() {
|
|
$('#btn_send_otp').prop('disabled', true);
|
|
|
|
return ajax_request({
|
|
url: '<?php echo $server_url?>setting/api/engine/request_reset_otp.php',
|
|
autoPrepare: false,
|
|
noLoading: false,
|
|
data: { json: JSON.stringify({
|
|
otp: document.getElementById('session-context').dataset.otp,
|
|
company_id: document.getElementById('session-context').dataset.companyId,
|
|
action: 'read',
|
|
})},
|
|
onSuccess: function(res) {
|
|
// Move to step 2
|
|
$('#forgot_step1').addClass('d-none');
|
|
$('#forgot_step2').removeClass('d-none');
|
|
$('#modal_masked_email2').text(res.masked_email);
|
|
$('#modal_reference').text(res.reference);
|
|
}
|
|
}).catch(function() {
|
|
$('#btn_send_otp').prop('disabled', false);
|
|
});
|
|
}
|
|
|
|
function on_reset_password_input(pw) {
|
|
clearTimeout(reset_pw_debounce);
|
|
if (!pw) { reset_strength_ui('reset'); update_reset_button(); return; }
|
|
reset_pw_debounce = setTimeout(() => check_strength(pw, 'reset'), 350);
|
|
}
|
|
|
|
function check_reset_confirm_match() {
|
|
const np = $('#reset_new_password').val();
|
|
const cp = $('#reset_confirm_password').val();
|
|
if (!cp) { $('#reset_match_label').text(''); return; }
|
|
if (np === cp) {
|
|
$('#reset_match_label').html('<span style="color:#198754;">✓ Passwords match</span>');
|
|
} else {
|
|
$('#reset_match_label').html('<span style="color:#dc3545;">✗ Passwords do not match</span>');
|
|
}
|
|
update_reset_button();
|
|
}
|
|
|
|
function update_reset_button() {
|
|
const otp = $('#reset_otp').val();
|
|
const np = $('#reset_new_password').val();
|
|
const cp = $('#reset_confirm_password').val();
|
|
const ready = otp.length === 6 && np && cp && (np === cp) && reset_pw_score >= 3;
|
|
$('#btn_reset_confirm').prop('disabled', !ready);
|
|
}
|
|
|
|
function confirm_reset_password() {
|
|
return ajax_request({
|
|
url: '<?php echo $server_url?>setting/api/engine/reset_password_otp.php',
|
|
autoPrepare: false,
|
|
data: { json: JSON.stringify({
|
|
session_otp: document.getElementById('session-context').dataset.otp,
|
|
company_id: document.getElementById('session-context').dataset.companyId,
|
|
action: 'update',
|
|
otp: $('#reset_otp').val(),
|
|
new_password: $('#reset_new_password').val(),
|
|
confirm_password: $('#reset_confirm_password').val(),
|
|
})},
|
|
onSuccess: function() {
|
|
$('#forgotModal').modal('hide');
|
|
bootbox.alert(
|
|
'Password reset successfully. You will be logged out now.',
|
|
function() { window.location.href = '<?php echo $server_url?>index.php'; }
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// SHARED: strength check (used by both change & reset)
|
|
// prefix = 'pw' | 'reset'
|
|
// ═══════════════════════════════════════════════════════
|
|
function check_strength(pw, prefix) {
|
|
// Abort pending request
|
|
if (prefix === 'pw' && pw_check_xhr) pw_check_xhr.abort();
|
|
if (prefix === 'reset' && reset_pw_check_xhr) reset_pw_check_xhr.abort();
|
|
|
|
const xhr = $.ajax({
|
|
url: '<?php echo $server_url?>setting/api/engine/check_password.php',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: { json: JSON.stringify({
|
|
otp: document.getElementById('session-context').dataset.otp,
|
|
company_id: document.getElementById('session-context').dataset.companyId,
|
|
action: 'read',
|
|
password: pw,
|
|
})},
|
|
headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') },
|
|
success: function(res) {
|
|
const score = res.score ?? -1;
|
|
|
|
if (prefix === 'pw') pw_score = score;
|
|
if (prefix === 'reset') reset_pw_score = score;
|
|
|
|
const bar = prefix === 'pw' ? '#pw_strength_bar' : '#reset_strength_bar';
|
|
const label = prefix === 'pw' ? '#pw_strength_label' : '#reset_strength_label';
|
|
const feedback = prefix === 'pw' ? '#pw_feedback' : '#reset_feedback';
|
|
|
|
if (score < 0) { reset_strength_ui(prefix); return; }
|
|
|
|
const lvl = STRENGTH_LEVELS[score];
|
|
$(bar).css({ width: lvl.pct + '%', backgroundColor: lvl.color });
|
|
$(label).text(lvl.label).css('color', lvl.color);
|
|
$(feedback).text(res.feedback || '');
|
|
|
|
if (prefix === 'pw') { check_confirm_match(); update_pw_button(); }
|
|
if (prefix === 'reset') { check_reset_confirm_match(); update_reset_button(); }
|
|
}
|
|
});
|
|
|
|
if (prefix === 'pw') pw_check_xhr = xhr;
|
|
if (prefix === 'reset') reset_pw_check_xhr = xhr;
|
|
}
|
|
|
|
function reset_strength_ui(prefix) {
|
|
if (prefix === 'pw') pw_score = -1;
|
|
if (prefix === 'reset') reset_pw_score = -1;
|
|
|
|
const bar = prefix === 'pw' ? '#pw_strength_bar' : '#reset_strength_bar';
|
|
const label = prefix === 'pw' ? '#pw_strength_label' : '#reset_strength_label';
|
|
const feedback = prefix === 'pw' ? '#pw_feedback' : '#reset_feedback';
|
|
|
|
$(bar).css({ width: '0%', backgroundColor: '' });
|
|
$(label).text('—').css('color', '');
|
|
$(feedback).text('');
|
|
}
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// Shared: toggle show/hide password
|
|
// ═══════════════════════════════════════════════════════
|
|
$(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');
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|