tienquan88

index.php

Jul 30th, 2025
233
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 19.40 KB | Source Code | 0 0
  1. <?php
  2. // index.php
  3. session_start();
  4. if (empty($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
  5.     header('Location: login.php');
  6.     exit;
  7. }
  8. if (!isset($_SESSION['csrf_token'])) {
  9.     $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
  10. }
  11. $csrfToken = $_SESSION['csrf_token'];
  12.  
  13. function loadEnv(string $path): array {
  14.     $vars = [];
  15.     if (!file_exists($path)) return $vars;
  16.     foreach (file($path, FILE_IGNORE_NEW_LINES|FILE_SKIP_EMPTY_LINES) as $line) {
  17.         $line = trim($line);
  18.         if ($line === '' || $line[0] === '#' || strpos($line,'=')===false) continue;
  19.         list($n,$v) = explode('=',$line,2);
  20.         $vars[trim($n)] = trim($v," \t\n\r\0\x0B'\"");
  21.     }
  22.     return $vars;
  23. }
  24. $env    = loadEnv(__DIR__.'/.env');
  25. $QR_VAL = $env['QR'] ?? '';
  26. ?>
  27. <!DOCTYPE html>
  28. <html lang="vi">
  29. <head>
  30.   <meta charset="UTF-8">
  31.   <title>Giao diện tạo QR</title>
  32.   <meta name="viewport" content="width=device-width,initial-scale=1.0">
  33.   <meta name="csrf-token" content="<?=htmlspecialchars($csrfToken,ENT_QUOTES)?>">
  34.   <style>
  35.     body{margin:0;padding:0;font-family:Arial,sans-serif;background:#f5f5f5;color:#222}
  36.     .dark-mode{background:#222;color:#ddd}
  37.     .dark-mode input,.dark-mode select,.dark-mode button,.dark-mode .col {
  38.       background:#333;color:#ddd;border-color:#555
  39.     }
  40.     #btnThemeIcon{position:fixed;top:12px;right:12px;font-size:24px;background:none;border:none;cursor:pointer}
  41.     #infoPanel{position:fixed;top:12px;left:12px;background:rgba(255,255,255,0.8);padding:8px 12px;border-radius:6px;
  42.       font-size:14px;line-height:1.4}
  43.     #popup{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
  44.       background:#fff;padding:16px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.2);
  45.       width:90vw;max-width:600px}
  46.     .input-group{display:flex;margin:8px 0}
  47.     .input-group input#calc{flex:1;padding:8px;font-size:16px;border:2px solid #ccc;border-radius:4px;transition:0.3s}
  48.     .input-group input#calc:focus{border-color:#007bff;box-shadow:0 0 6px rgba(0,123,255,0.5);outline:none}
  49.     .input-group button#btnSend{margin-left:4px;padding:8px 12px;font-size:16px;border:none;border-radius:4px;
  50.       background:#28a745;color:#fff;cursor:pointer;transition:0.2s}
  51.     .input-group button#btnSend:hover{opacity:.9}.input-group button#btnSend:active{transform:scale(0.98)}
  52.     #sendStatus{font-size:13px;color:#333;min-height:1em;margin-bottom:8px}
  53.     .bank-select{display:flex;gap:8px;margin:8px 0}
  54.     .bank-select input,.bank-select select{flex:1;padding:6px;font-size:14px;border:2px solid #ccc;border-radius:4px}
  55.     #qrContainer{text-align:center;margin:12px 0}
  56.     #qrContainer img{max-width:200px;border:1px solid #ccc;padding:4px;background:#fff;display:none}
  57.     .qr-actions{display:flex;justify-content:space-between;margin:8px 0}
  58.     .qr-actions button{flex:1;margin:0 4px;padding:6px;font-size:14px;border:none;border-radius:4px;
  59.       color:#fff;cursor:pointer;transition:0.2s}
  60.     #btnPersonal{background:#007bff}#btnCustom{background:#17a2b8}#btnCloseQR{background:#dc3545}
  61.     .qr-actions button:hover{opacity:.9}
  62.     .columns{display:flex;gap:12px;margin-top:12px}
  63.     .col{flex:1;background:#fafafa;padding:8px;border-radius:4px;max-height:250px;overflow:auto}
  64.     .col header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
  65.     .col ul{list-style:none;padding:0;margin:0}
  66.     .col li{font-size:13px;margin:4px 0;display:flex;justify-content:space-between;gap:8px}
  67.     .col .time{color:#555;margin-right:6px}.col .status-emoji{margin-right:4px}.col .amount{font-weight:bold}
  68.     #toggleAmt{background:none;border:none;cursor:pointer;font-size:18px}
  69.     #popupMessage{position:absolute;top:10px;right:10px;padding:5px 10px;border-radius:5px;
  70.       background:rgba(0,0,0,0.6);color:lightgreen;font-size:12px;display:none}
  71.   </style>
  72. </head>
  73. <body id="body">
  74.   <button id="btnThemeIcon">🎨</button>
  75.   <div id="infoPanel">
  76.     <div id="clock-day">–</div>
  77.     <div id="clock-time">Giờ: –</div>
  78.     <div id="start-time">Bắt đầu: –</div>
  79.     <div id="elapsed-time">Chạy: –</div>
  80.   </div>
  81.  
  82.   <div id="popup">
  83.     <div id="popupMessage"></div>
  84.  
  85.     <div class="input-group">
  86.       <input id="calc" type="text" placeholder="Nhập số, phép tính, hoặc /lệnh">
  87.       <button id="btnSend">Gửi</button>
  88.     </div>
  89.     <div id="sendStatus"><em>Chưa gửi gì.</em></div>
  90.  
  91.     <div class="bank-select">
  92.       <input id="account" type="text" placeholder="Số tài khoản">
  93.       <select id="bank"><option>Đang tải…</option></select>
  94.     </div>
  95.  
  96.     <div id="qrContainer">
  97.       <img id="qrImage" alt="QR Code">
  98.     </div>
  99.     <div class="qr-actions">
  100.       <button id="btnPersonal">QR Cá Nhân</button>
  101.       <button id="btnCustom">QR Tùy Chỉnh</button>
  102.       <button id="btnCloseQR">Tắt QR</button>
  103.     </div>
  104.  
  105.     <div class="columns">
  106.       <div class="col" id="historyCol">
  107.         <header>
  108.           <strong>Lịch sử GD</strong>
  109.           <button id="toggleAmt">👁️</button>
  110.         </header>
  111.         <ul id="historyList">
  112.           <li><em>Chưa có giao dịch.</em></li>
  113.         </ul>
  114.       </div>
  115.       <div class="col" id="queueCol">
  116.         <header><strong>Danh sách đợi</strong></header>
  117.         <ul id="queueList">
  118.           <li><em>Đang tải…</em></li>
  119.         </ul>
  120.       </div>
  121.     </div>
  122.   </div>
  123.  
  124.   <script>
  125.     const csrfToken = document.head.querySelector('[name="csrf-token"]').content;
  126.     const QR_VAL     = '<?=htmlspecialchars($QR_VAL,ENT_QUOTES)?>';
  127.  
  128.     let historyList  = [], isProcessing = false, showAmt = false;
  129.     const pageStart  = new Date();
  130.     const days       = ['Chủ Nhật','Thứ Hai','Thứ Ba','Thứ Tư','Thứ Năm','Thứ Sáu','Thứ Bảy'];
  131.     const calc       = document.getElementById('calc');
  132.  
  133.     function showMessage(msg, isErr=false) {
  134.       const d = document.getElementById('popupMessage');
  135.       d.style.color   = isErr ? 'red' : 'lightgreen';
  136.       d.textContent   = msg;
  137.       d.style.display = 'block';
  138.       setTimeout(() => d.style.display = 'none', 3000);
  139.     }
  140.  
  141.     const formatThousands   = x => x.toString().replace(/\B(?=(\d{3})+(?!\d))/g,'.');
  142.     const unformatThousands = x => x.toString().replace(/\./g,'');
  143.  
  144.     function updateInfo() {
  145.       const now = new Date();
  146.       document.getElementById('clock-day').textContent =
  147.         days[now.getDay()] + ', ' + now.toLocaleDateString('vi-VN');
  148.       document.getElementById('clock-time').textContent =
  149.         'Giờ: ' + now.toLocaleTimeString('vi-VN');
  150.       document.getElementById('start-time').textContent =
  151.         'Bắt đầu: ' + pageStart.toLocaleTimeString('vi-VN');
  152.       let diff = Math.floor((now - pageStart)/1000),
  153.           h = String(Math.floor(diff/3600)).padStart(2,'0'),
  154.           m = String(Math.floor((diff%3600)/60)).padStart(2,'0'),
  155.           s = String(diff%60).padStart(2,'0');
  156.       document.getElementById('elapsed-time').textContent =
  157.         `Chạy: ${h}:${m}:${s}`;
  158.     }
  159.     setInterval(updateInfo, 1000);
  160.     updateInfo();
  161.  
  162.     function renderHistory() {
  163.       const ul = document.getElementById('historyList');
  164.       ul.innerHTML = '';
  165.       if (!historyList.length) {
  166.         ul.innerHTML = '<li><em>Chưa có giao dịch.</em></li>';
  167.         return;
  168.       }
  169.       historyList.forEach(h => {
  170.         const li = document.createElement('li');
  171.         const amt = showAmt ? formatThousands(h.amount) : '••••';
  172.         li.innerHTML = `
  173.           <span>
  174.             <span class="time">${h.time}</span>
  175.             <span class="status-emoji">${h.icon}</span>
  176.           </span>
  177.           <span class="amount">${amt}</span>`;
  178.         ul.append(li);
  179.       });
  180.     }
  181.  
  182.     async function loadQueue() {
  183.       const r = await fetch('queue.php?action=list');
  184.       return r.ok ? await r.json() : [];
  185.     }
  186.  
  187.     async function renderQueue() {
  188.       const data = await loadQueue();
  189.       const ul = document.getElementById('queueList');
  190.       ul.innerHTML = '';
  191.       if (!data.length) {
  192.         ul.innerHTML = '<li><em>Không có tin lỗi</em></li>';
  193.         return;
  194.       }
  195.       data.forEach((it, idx) => {
  196.         const li = document.createElement('li');
  197.         li.innerHTML = `
  198.           <span class="time">${new Date(it.time).toLocaleString()}</span>
  199.           <span style="color:green;cursor:pointer" data-edit-idx="${idx}">Sửa</span>
  200.           <span style="color:blue;cursor:pointer" data-del-idx="${idx}">Xóa</span>`;
  201.         ul.append(li);
  202.       });
  203.  
  204.       // Xóa
  205.       ul.querySelectorAll('[data-del-idx]').forEach(el => {
  206.         el.onclick = async () => {
  207.           if (!confirm('Xác nhận xóa?')) return;
  208.           await fetch(`queue.php?action=delete&index=${el.dataset.delIdx}`, {
  209.             method: 'POST',
  210.             headers: { 'X-CSRF-Token': csrfToken }
  211.           });
  212.           renderQueue();
  213.         };
  214.       });
  215.  
  216.       // Sửa
  217.       ul.querySelectorAll('[data-edit-idx]').forEach(el => {
  218.         el.onclick = async () => {
  219.           const idx = el.dataset.editIdx;
  220.           const old = data[idx].payload;
  221.           const neu = prompt('Nhập payload mới:', old);
  222.           if (neu === null || neu.trim() === '' || neu === old) return;
  223.           await fetch(`queue.php?action=update&index=${idx}`, {
  224.             method: 'POST',
  225.             headers: {
  226.               'Content-Type': 'application/x-www-form-urlencoded',
  227.               'X-CSRF-Token': csrfToken
  228.             },
  229.             body: `payload=${encodeURIComponent(neu)}&time=${encodeURIComponent(data[idx].time)}`
  230.           });
  231.           renderQueue();
  232.         };
  233.       });
  234.     }
  235.  
  236.     // Khởi tạo lịch sử từ localStorage
  237.     (function initHistory() {
  238.       const saved = JSON.parse(localStorage.getItem('send_history') || '[]');
  239.       historyList = saved;
  240.       renderHistory();
  241.     })();
  242.  
  243.     // Refresh queue định kỳ
  244.     renderQueue();
  245.     setInterval(renderQueue, 15000);
  246.  
  247.     // --- Server-Sent Events: nhận giao dịch real-time từ events.php ---
  248.     let lastId = parseInt(localStorage.getItem('lastId') || '-1', 10);
  249.     const evt = new EventSource('events.php?lastId=' + lastId);
  250.  
  251.     evt.onmessage = e => {
  252.       const tx = JSON.parse(e.data);
  253.       lastId = parseInt(e.lastEventId, 10);
  254.       localStorage.setItem('lastId', lastId);
  255.  
  256.       // Thêm vào lịch sử và hiển thị
  257.       historyList.unshift({
  258.         time: tx.time,
  259.         amount: tx.amount,
  260.         icon: '✅'
  261.       });
  262.       localStorage.setItem('send_history', JSON.stringify(historyList));
  263.       renderHistory();
  264.  
  265.       showMessage(
  266.         `Giao dịch mới: ${tx.date} ${tx.time} – ${Number(tx.amount).toLocaleString('vi-VN')} VND`
  267.       );
  268.     };
  269.  
  270.     evt.onerror = err => {
  271.       console.error('SSE error:', err);
  272.       evt.close();
  273.     };
  274.  
  275.     // Các hàm gửi, xử lý queue, format, xử lý input...
  276.     async function sendDirect(v) {
  277.       isProcessing = true;
  278.       document.getElementById('sendStatus').textContent = `Đang gửi: ${formatThousands(v)}`;
  279.       try {
  280.         const res = await fetch('send.php', {
  281.           method:'POST',
  282.           headers:{
  283.             'Content-Type':'application/x-www-form-urlencoded',
  284.             'X-CSRF-Token':csrfToken
  285.           },
  286.           body:`result=${encodeURIComponent(v)}&time=${encodeURIComponent(new Date().toISOString())}`
  287.         });
  288.         const txt = await res.text();
  289.         if (txt.toLowerCase().includes('thành công')) {
  290.           historyList.unshift({ time:new Date().toLocaleTimeString('vi-VN'), icon:'✅', amount:v });
  291.           renderHistory();
  292.         } else throw 'Lỗi server';
  293.       } catch {
  294.         await fetch('queue.php?action=add',{
  295.           method:'POST',
  296.           headers:{ 'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':csrfToken },
  297.           body:`payload=${encodeURIComponent(v)}&time=${encodeURIComponent(new Date().toISOString())}`
  298.         });
  299.         renderQueue();
  300.       } finally {
  301.         isProcessing = false;
  302.         document.getElementById('sendStatus').textContent = '';
  303.         processQueue();
  304.       }
  305.     }
  306.  
  307.     async function processQueue() {
  308.       if (isProcessing) return;
  309.       isProcessing = true;
  310.       while (true) {
  311.         const q = await loadQueue();
  312.         if (!q.length) break;
  313.         const it = q[0];
  314.         document.getElementById('sendStatus').textContent = `Resend: ${formatThousands(it.payload)}`;
  315.         try {
  316.           const res = await fetch('send.php', {
  317.             method:'POST',
  318.             headers:{ 'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':csrfToken },
  319.             body:`result=${encodeURIComponent(it.payload)}&time=${encodeURIComponent(it.time)}`
  320.           });
  321.           const txt = await res.text();
  322.           if (txt.toLowerCase().includes('thành công')) {
  323.             await fetch(`queue.php?action=delete&index=0`,{ method:'POST','X-CSRF-Token':csrfToken });
  324.             historyList.unshift({ time:new Date().toLocaleTimeString('vi-VN'), icon:'🔄', amount:it.payload });
  325.             renderHistory();
  326.             renderQueue();
  327.           } else throw 'Server lỗi';
  328.         } catch {
  329.           break;
  330.         }
  331.       }
  332.       isProcessing = false;
  333.       document.getElementById('sendStatus').textContent = '';
  334.     }
  335.  
  336.     function sendToServer(cmd) {
  337.       fetch('send.php', {
  338.         method:'POST',
  339.         headers:{ 'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':csrfToken },
  340.         body:`result=${encodeURIComponent(cmd)}`
  341.       })
  342.       .then(r=>r.text())
  343.       .then(txt=>{
  344.         const now = new Date().toLocaleTimeString('vi-VN');
  345.         const ok  = txt.toLowerCase().includes('thành công');
  346.         historyList.unshift({
  347.           time: now,
  348.           icon: ok?'✅':'❌',
  349.           amount: ok?cmd:`${cmd}: ${txt.trim()}`
  350.         });
  351.         renderHistory();
  352.         showMessage(ok?'✅ Lệnh thành công':`❌ ${txt.trim()}`, !ok);
  353.       })
  354.       .catch(e=>{
  355.         const now = new Date().toLocaleTimeString('vi-VN');
  356.         historyList.unshift({ time:now, icon:'⚠️', amount:`Lỗi: ${e}` });
  357.         renderHistory();
  358.         showMessage('⚠️ Lỗi khi gửi', true);
  359.       });
  360.     }
  361.  
  362.     async function handleInput(raw) {
  363.       const v0 = raw.trim();
  364.       if (!v0) return;
  365.       calc.value = ''; calc.focus();
  366.  
  367.       if (v0.startsWith('/')) {
  368.         sendToServer(v0);
  369.         return;
  370.       }
  371.       const calcPattern = /^\d+(\s*[\+\-\*\/]\s*\d+)+$/;
  372.       if (calcPattern.test(v0)) {
  373.         try { const r = eval(v0); if (!isNaN(r)) calc.value = r; }
  374.         catch{}
  375.         calc.focus();
  376.         return;
  377.       }
  378.       const num = unformatThousands(v0);
  379.       const q = await loadQueue();
  380.       if (!isProcessing && !q.length) {
  381.         await sendDirect(num);
  382.       } else {
  383.         await fetch('queue.php?action=add',{
  384.           method:'POST',
  385.           headers:{ 'Content-Type':'application/x-www-form-urlencoded','X-CSRF-Token':csrfToken },
  386.           body:`payload=${encodeURIComponent(num)}&time=${encodeURIComponent(new Date().toISOString())}`
  387.         });
  388.         renderQueue();
  389.       }
  390.     }
  391.  
  392.     calc.addEventListener('input', e=>{
  393.       const el = e.target, v = el.value, pos = el.selectionStart;
  394.       if (v.startsWith('/') || /[+\-*/()]/.test(v)) return;
  395.       const raw = v.replace(/\D/g,''), fmt = raw.replace(/\B(?=(\d{3})+(?!\d))/g,'.');
  396.       const diff = fmt.length - v.length;
  397.       el.value = fmt;
  398.       el.setSelectionRange(pos+diff,pos+diff);
  399.     });
  400.  
  401.     document.getElementById('toggleAmt').onclick = ()=>{
  402.       showAmt = !showAmt;
  403.       document.getElementById('toggleAmt').textContent = showAmt?'👁️':'🙈';
  404.       renderHistory();
  405.     };
  406.     document.getElementById('btnSend').onclick = ()=> handleInput(calc.value);
  407.     document.body.addEventListener('keydown', e=>{
  408.       if (e.key==='Enter'){ e.preventDefault(); handleInput(calc.value); }
  409.       if (e.key==='Escape'){ e.preventDefault(); calc.value=''; calc.focus(); }
  410.     });
  411.  
  412.     const qrImg = document.getElementById('qrImage');
  413.     function openQR(type,withAmount){
  414.       const acc  = encodeURIComponent(document.getElementById('account').value.trim());
  415.       const bin  = encodeURIComponent(document.getElementById('bank').value);
  416.       const amt0 = withAmount? unformatThousands(calc.value.trim()) : 0;
  417.       const data = `${QR_VAL}?bank=${bin}&account=${acc}` + (withAmount?`&amount=${amt0}`:'');
  418.       qrImg.src = `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(data)}&size=200x200`;
  419.       qrImg.style.display = 'block';
  420.     }
  421.     document.getElementById('btnPersonal').onclick = ()=> openQR('personal',false);
  422.     document.getElementById('btnCustom').onclick   = ()=> openQR('custom',true);
  423.     document.getElementById('btnCloseQR').onclick  = ()=> qrImg.style.display='none';
  424.  
  425.     window.addEventListener('load',()=>{
  426.       fetch('https://api.vietqr.io/v2/banks')
  427.         .then(r=>r.json()).then(o=>{
  428.           const sel = document.getElementById('bank');
  429.           sel.innerHTML = '';
  430.           o.data.forEach(b=> sel.append(new Option(b.shortName,b.bin)));
  431.         })
  432.         .catch(()=> showMessage('⚠️ Không tải được danh sách ngân hàng',true));
  433.       document.getElementById('toggleAmt').textContent = showAmt ? '👁️' : '🙈';
  434.       renderHistory();
  435.       renderQueue();
  436.       processQueue();
  437.     });
  438.  
  439.     const bodyEl = document.getElementById('body');
  440.     if (localStorage.getItem('darkMode')==='true') bodyEl.classList.add('dark-mode');
  441.     document.getElementById('btnThemeIcon').onclick = ()=>{
  442.       bodyEl.classList.toggle('dark-mode');
  443.       localStorage.setItem('darkMode',bodyEl.classList.contains('dark-mode'));
  444.     };
  445.  
  446.     window.addEventListener('beforeunload', e=>{
  447.       e.preventDefault();
  448.       e.returnValue = 'Bạn có chắc chắn muốn tải lại trang?';
  449.     });
  450. ;(function(){
  451.   // SSE: nhận real-time
  452.   let sseLastId = parseInt(localStorage.getItem('lastId')||'-1',10);
  453.   const evt = new EventSource('QR/events.php?lastId='+sseLastId);
  454.  
  455.   evt.onmessage = e => {
  456.     try {
  457.       const tx = JSON.parse(e.data);
  458.       sseLastId = parseInt(e.lastEventId,10);
  459.       localStorage.setItem('lastId', sseLastId);
  460.  
  461.       historyList.unshift({
  462.         time: new Date(tx.time).toLocaleTimeString('vi-VN'),
  463.         amount: tx.amount,
  464.         icon: '✅'
  465.       });
  466.       localStorage.setItem('send_history', JSON.stringify(historyList));
  467.       renderHistory();
  468.       showMessage(`Giao dịch mới (SSE): ${tx.amount} VND`);
  469.     } catch(e) { console.error(e); }
  470.   };
  471.   evt.onerror = err => { console.error('SSE error', err); evt.close(); };
  472.  
  473.   // Polling fallback
  474.   let pollLastId = parseInt(localStorage.getItem('lastPollId')||'-1',10);
  475.   async function pollNew(){
  476.     try {
  477.       const res = await fetch('QR/get.php?lastId='+pollLastId, {
  478.         headers: {'X-CSRF-Token': csrfToken}
  479.       });
  480.       if (!res.ok) return;
  481.       const list = await res.json();
  482.       list.forEach(tx=>{
  483.         historyList.unshift({
  484.           time: new Date(tx.time).toLocaleTimeString('vi-VN'),
  485.           amount: tx.amount,
  486.           icon: '✅'
  487.         });
  488.         renderHistory();
  489.         showMessage(`Giao dịch mới (poll): ${tx.amount} VND`);
  490.         pollLastId = tx.id;
  491.       });
  492.       if (list.length) localStorage.setItem('lastPollId', pollLastId);
  493.     } catch(e) { console.error(e); }
  494.   }
  495.   pollNew();
  496.   setInterval(pollNew, 5000);
  497. })();
  498.  
  499.   </script>
  500. </body>
  501. </html>
  502.  
Advertisement
Add Comment
Please, Sign In to add comment