- .htaccess: refuse .git, .env, docker, nodejs, sdlc*, scripts, archives and app internals; security headers, HSTS over TLS, optional HTTPS redirect - uploads served through app/file.php to signed-in users only - Apache/PHP hardening config for the container (ServerTokens, expose_php) - least-privilege DB account and APP_SECRET_KEY via docker/php/provision.php; SMTP passwords re-encrypted with a random IV (secret_box.php) - Socket.IO rooms from a PHP-signed token; /emit and cron refuse empty secrets - escape notification text; CLI guards on build scripts; no fixed demo password
150 lines
6.0 KiB
PHP
150 lines
6.0 KiB
PHP
<div class="row">
|
|
<div class="col-12">
|
|
<footer class="text-center py-2 mt-6 text-secondary ">
|
|
<p class="mb-0">Copyright © 2026 BRN WMS. All rights reserved.</p>
|
|
</footer>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<?php
|
|
// Signed socket token: the Node server joins rooms from these claims only, so a
|
|
// browser cannot pick another company's room. Must match verifySocketToken() in
|
|
// nodejs/server.js (key derived from NODE_EMIT_SECRET, HMAC-SHA256, base64url).
|
|
$_socket_token = '';
|
|
if (defined('NODE_EMIT_SECRET') && NODE_EMIT_SECRET !== '' && !empty($_SESSION['login_company_id'])) {
|
|
$_b64url = fn ($s) => rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
|
|
$_payload = $_b64url(json_encode([
|
|
'c' => (int)$_SESSION['login_company_id'],
|
|
'u' => (int)($_SESSION['login_user_id'] ?? 0),
|
|
'r' => (string)($_SESSION['login_role'] ?? 'viewer'),
|
|
'exp' => time() + 8 * 3600,
|
|
]));
|
|
$_socket_key = hash_hmac('sha256', 'socket-token', NODE_EMIT_SECRET, true);
|
|
$_socket_token = $_payload . '.' . $_b64url(hash_hmac('sha256', $_payload, $_socket_key, true));
|
|
}
|
|
?>
|
|
<!-- ── Real-time WebSocket connection ──────────────────────────────────────── -->
|
|
<!-- Socket.io client is served by the Node.js server itself -->
|
|
<script src="<?php echo htmlspecialchars(NODE_PUBLIC_URL, ENT_QUOTES, 'UTF-8'); ?>/socket.io/socket.io.js"></script>
|
|
<script>
|
|
(function () {
|
|
// company_id is set in include_topbar.php as a JS global
|
|
if (typeof company_id === 'undefined' || !company_id) return;
|
|
if (typeof io === 'undefined') return;
|
|
|
|
window._socket = io(<?php echo json_encode(NODE_PUBLIC_URL, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES); ?>, {
|
|
auth: { token: <?php echo json_encode($_socket_token, JSON_HEX_TAG); ?> },
|
|
reconnection: true,
|
|
reconnectionDelay: 2000,
|
|
});
|
|
|
|
window._socket.on('connect', function () {
|
|
console.log('[socket] connected', window._socket.id);
|
|
});
|
|
|
|
window._socket.on('disconnect', function () {
|
|
console.log('[socket] disconnected');
|
|
});
|
|
|
|
// ── Real-time notifications ───────────────────────────────────────────────
|
|
// PHP emits { title, message, type } where type is 'success'|'info'|'warning'|'danger'
|
|
window._socket.on('notification', function (data) {
|
|
var title = data.title || '';
|
|
var message = data.message || '';
|
|
var type = data.type || 'info';
|
|
show_toast(title, message, type);
|
|
if (typeof add_notification === 'function') {
|
|
add_notification(title, message, type);
|
|
}
|
|
});
|
|
})();
|
|
|
|
// ── show_toast(title, message, type) ─────────────────────────────────────────
|
|
// Renders a Bootstrap 5 toast in the top-right corner.
|
|
// type: 'success' | 'info' | 'warning' | 'danger'
|
|
function show_toast(title, message, type) {
|
|
type = type || 'info';
|
|
|
|
var icon_map = {
|
|
success: 'ti-circle-check text-success',
|
|
info: 'ti-info-circle text-info',
|
|
warning: 'ti-alert-triangle text-warning',
|
|
danger: 'ti-circle-x text-danger',
|
|
};
|
|
var icon = icon_map[type] || icon_map.info;
|
|
|
|
// Notification text is data, never markup.
|
|
var esc = function (v) {
|
|
return String(v).replace(/[&<>"']/g, function (c) {
|
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
});
|
|
};
|
|
|
|
var container = document.getElementById('_toast_container');
|
|
if (!container) {
|
|
container = document.createElement('div');
|
|
container.id = '_toast_container';
|
|
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
|
container.style.zIndex = 9999;
|
|
document.body.appendChild(container);
|
|
}
|
|
|
|
var id = '_toast_' + Date.now();
|
|
var el = document.createElement('div');
|
|
el.id = id;
|
|
el.className = 'toast align-items-center border-0 shadow-sm';
|
|
el.setAttribute('role', 'alert');
|
|
el.setAttribute('aria-live', 'assertive');
|
|
el.innerHTML = [
|
|
'<div class="d-flex">',
|
|
' <div class="toast-body d-flex align-items-start gap-2">',
|
|
' <i class="ti ' + icon + ' fs-5 mt-1 flex-shrink-0"></i>',
|
|
' <div>',
|
|
(title ? '<div class="fw-semibold lh-sm">' + esc(title) + '</div>' : ''),
|
|
(message ? '<div class="small text-muted">' + esc(message) + '</div>' : ''),
|
|
' </div>',
|
|
' </div>',
|
|
' <button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button>',
|
|
'</div>',
|
|
].join('');
|
|
|
|
container.appendChild(el);
|
|
|
|
var toast = new bootstrap.Toast(el, { delay: 5000 });
|
|
toast.show();
|
|
|
|
el.addEventListener('hidden.bs.toast', function () { el.remove(); });
|
|
}
|
|
</script>
|
|
|
|
<!-- Stock rows modal — used by: order/order.php, order/return.php, po/po.php, po/supplier_returns.php (via show_stock_rows() in custom.js) -->
|
|
<div class="modal fade" id="stock_rows_modal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="stock_rows_modal_title">Stock Transactions</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th class="ps-4">Type</th>
|
|
<th>Product</th>
|
|
<th>Warehouse</th>
|
|
<th>Location</th>
|
|
<th>Lot</th>
|
|
<th class="text-end">Qty</th>
|
|
<th>Status</th>
|
|
<th>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="stock_rows_tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|