implement JS password scoring, td_stock approve logicc

This commit is contained in:
Thanakorn S
2026-04-30 15:14:42 +07:00
parent db5c47b6ca
commit ac4fd9a5ad
3147 changed files with 1298 additions and 343 deletions
@@ -1,7 +0,0 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/PasswordManager.php';
$pm = new PasswordManager($pdo1, $include_url);
$pm->handleCheck($data);
@@ -0,0 +1,7 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
$csm = new CompanySettingManager($pdo1, $company_id);
$csm->handle($data);
+3 -2
View File
@@ -60,8 +60,9 @@
db_check($sth, $answer);
// ── Refresh session ───────────────────────────────────────
$_SESSION['login_name'] = trim($data['name'] ?? '');
$_SESSION['login_surname'] = trim($data['surname'] ?? '');
$_SESSION['login_name'] = trim($data['name'] ?? '');
$_SESSION['login_surname'] = trim($data['surname'] ?? '');
$_SESSION['login_profile_picture'] = $db_picture;
$answer['success'] = 1;
$answer['message'] = 'Profile updated.';
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* switch_branch.php — Switch the active company for the current session.
*
* action: 'read' → return list of companies the user belongs to
* action: 'update' → switch to the requested company_id
*
* Both actions verify that the user is actually a member of the target company
* via company_map_user before touching the session.
*/
session_start();
require '../../../assets/utils/db_auth.php';
$action = $data['action'] ?? '';
// ── READ: return all companies this user belongs to ───────────────────────────
if ($action === 'read') {
$sth = $pdo1->prepare(
"SELECT cl.company_id, cl.company_name, cl.branch, cmu.role
FROM company_map_user cmu
JOIN company_list cl ON cl.company_id = cmu.company_id
WHERE cmu.user_id = :user_id
ORDER BY cl.company_name ASC"
);
$sth->execute([':user_id' => $user_id]);
$companies = $sth->fetchAll(PDO::FETCH_ASSOC);
$answer['success'] = 1;
$answer['output'] = $companies;
$answer['current'] = (int) $_SESSION['login_company_id'];
exit(json_encode($answer));
}
// ── UPDATE: switch to a different company ─────────────────────────────────────
if ($action === 'update') {
$target_company_id = (int)($data['company_id'] ?? 0);
if (!$target_company_id) {
http_response_code(400);
$answer['message'] = 'Invalid company.';
exit(json_encode($answer));
}
// Verify user actually belongs to the requested company
$sth = $pdo1->prepare(
"SELECT company_id FROM company_map_user
WHERE company_id = :company_id AND user_id = :user_id
LIMIT 1"
);
$sth->execute([':company_id' => $target_company_id, ':user_id' => $user_id]);
if (!$sth->fetch()) {
http_response_code(403);
$answer['message'] = 'You do not have access to this company.';
exit(json_encode($answer));
}
$_SESSION['login_company_id'] = $target_company_id;
$answer['success'] = 1;
$answer['message'] = 'Switched successfully.';
exit(json_encode($answer));
}
http_response_code(400);
$answer['message'] = 'Invalid action.';
exit(json_encode($answer));
+26 -44
View File
@@ -5,6 +5,9 @@
?>
<body>
<!-- zxcvbn JS: client-side password strength scoring, no server round-trip -->
<script src="https://cdn.jsdelivr.net/npm/zxcvbn@4.4.2/dist/zxcvbn.js"></script>
<?php require '../include_topbar.php'; ?>
<?php require '../include_setting_sidebar.php'; ?>
@@ -318,12 +321,8 @@
// ═══════════════════════════════════════════════════════
// 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 pw_score = -1;
var reset_pw_score = -1;
var user_email = ''; // stored on retrieve, used for modal masking
function mask_email(email) {
@@ -448,9 +447,8 @@
// 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);
check_strength(pw, 'pw');
}
function check_confirm_match() {
@@ -474,6 +472,7 @@
}
function change_password() {
if (pw_score < 3) { bootbox.alert('Please choose a stronger password.'); return; }
return ajax_request({
url: '<?php echo $server_url?>setting/api/engine/change_password.php',
autoPrepare: false,
@@ -542,9 +541,8 @@
}
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);
check_strength(pw, 'reset');
}
function check_reset_confirm_match() {
@@ -568,6 +566,7 @@
}
function confirm_reset_password() {
if (reset_pw_score < 3) { bootbox.alert('Please choose a stronger password.'); return; }
return ajax_request({
url: '<?php echo $server_url?>setting/api/engine/reset_password_otp.php',
autoPrepare: false,
@@ -595,45 +594,28 @@
// 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 user_inputs = [
$('#name').val(), $('#surname').val(),
$('#username').val(), user_email
].filter(Boolean);
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;
const result = zxcvbn(pw, user_inputs);
const score = result.score;
if (prefix === 'pw') pw_score = score;
if (prefix === 'reset') reset_pw_score = score;
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';
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(result.feedback.warning || result.feedback.suggestions[0] || '');
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;
if (prefix === 'pw') { check_confirm_match(); update_pw_button(); }
if (prefix === 'reset') { check_reset_confirm_match(); update_reset_button(); }
}
function reset_strength_ui(prefix) {
+119
View File
@@ -0,0 +1,119 @@
<?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">System Configuration</h1>
<p class="mb-0">Per-company settings that control application behaviour</p>
</div>
</div>
</div>
</div>
<!-- Stock Settings -->
<div class="row">
<div class="col-12 col-lg-7">
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent border-bottom">
<h6 class="mb-0"><i class="ti ti-package me-2"></i>Stock Settings</h6>
</div>
<div class="card-body">
<div class="mb-4">
<label class="form-label fw-semibold">Default Stock Status</label>
<p class="text-muted small mb-2">
Controls whether new stock-in, stock-out, and transfer records are saved
as <strong>Draft</strong> (requires manual approval) or
<strong>Auto-approved</strong> (approved immediately on save).
Only approved records appear in reports and balance calculations.
</p>
<div class="d-flex flex-column gap-2" id="default_stock_status_group">
<div class="form-check">
<input class="form-check-input" type="radio" name="default_stock_status"
id="status_auto" value="1">
<label class="form-check-label" for="status_auto">
<span class="fw-semibold">Auto-approve</span>
<span class="text-muted ms-1 small">— records are approved immediately on save</span>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="default_stock_status"
id="status_draft" value="0">
<label class="form-check-label" for="status_draft">
<span class="fw-semibold">Draft</span>
<span class="text-muted ms-1 small">— records must be manually approved before affecting reports</span>
</label>
</div>
</div>
</div>
<button class="btn btn-primary" onclick="save_config();">
<i class="ti ti-device-floppy me-1"></i>Save
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
// ── Load ──────────────────────────────────────────────────────────────────
$(function() {
ajax_request({
url: server_url + 'setting/api/engine/company_setting.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var val = res.output.default_stock_status ?? 1;
$('input[name="default_stock_status"][value="' + val + '"]').prop('checked', true);
}
});
});
// ── Save ──────────────────────────────────────────────────────────────────
function save_config() {
var status = $('input[name="default_stock_status"]:checked').val();
if (status === undefined) {
bootbox.alert('Please select a default stock status.');
return;
}
ajax_request({
url: server_url + 'setting/api/engine/company_setting.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: {
default_stock_status: status,
},
onSuccess: function(res) {
bootbox.alert('Settings saved.');
}
});
}
</script>
</body>
</html>
+4 -4
View File
@@ -301,7 +301,7 @@
function open_invite_modal() {
$('#invite_email').val('').removeClass('is-invalid is-valid');
$('#invite_role').val('').removeClass('is-invalid');
new bootstrap.Modal('#inviteModal').show();
$('#inviteModal').modal('show');
}
function send_invite() {
@@ -330,7 +330,7 @@
action: 'create',
data: { invite_email: email, invite_role: role },
onSuccess: function (r) {
bootstrap.Modal.getInstance('#inviteModal')?.hide();
$('#inviteModal').modal('hide');
bootbox.alert(r.message || 'Invitation sent.');
load_users();
},
@@ -353,7 +353,7 @@
$('#edit_role').val(u.role);
$('#edit_avatar').attr('src', u.profile_picture ? img_base + u.profile_picture : avatar_ph);
new bootstrap.Modal('#editRoleModal').show();
$('#editRoleModal').modal('show');
}
function save_role() {
@@ -364,7 +364,7 @@
action: 'update',
data: { map_id: $('#edit_map_id').val(), role: $('#edit_role').val() },
onSuccess: function (r) {
bootstrap.Modal.getInstance('#editRoleModal')?.hide();
$('#editRoleModal').modal('hide');
bootbox.alert(r.message || 'Role updated.');
load_users();
},