From ad995352fc6be2193cef3bade257104c6801a62d Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sun, 3 May 2026 13:45:45 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20=D9=85=D9=8F=D8=B5=D8=A7=D8=AF?= =?UTF-8?q?=D9=8E=D9=82:=20=D8=AA=D8=AD=D8=AF=D9=8A=D8=AB=20=D8=A8=D8=B1?= =?UTF-8?q?=D9=85=D8=AC=D9=8A=20=D8=AC=D8=AF=D9=8A=D8=AF=202026-05-03=2013?= =?UTF-8?q?:45?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/shell.php | 302 ++++++++++++++++++++++++++++++- tests/Feature/AuthTest.php | 19 ++ tests/Unit/HmacTest.php | 44 +++++ tests/Unit/TaxValidationTest.php | 51 ++++++ 4 files changed, 411 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/AuthTest.php create mode 100644 tests/Unit/HmacTest.php create mode 100644 tests/Unit/TaxValidationTest.php diff --git a/public/shell.php b/public/shell.php index e4adfb6..53c0d72 100644 --- a/public/shell.php +++ b/public/shell.php @@ -60,6 +60,18 @@ المستخدمين + + + مراقبة المخاطر + + + + الإعدادات + +
+ +
+
@@ -148,6 +163,32 @@ const contentDiv = document.getElementById('page-content'); let currentChart = null; + function showToast(message, type = 'success') { + const container = document.getElementById('toast-container'); + const toast = document.createElement('div'); + const colors = { + success: 'bg-emerald-500 shadow-emerald-500/20', + error: 'bg-red-500 shadow-red-500/20', + warning: 'bg-yellow-500 shadow-yellow-500/20' + }; + + toast.className = `px-8 py-4 rounded-2xl text-white font-bold shadow-2xl transition-all duration-500 translate-y-10 opacity-0 pointer-events-auto flex items-center gap-3 ${colors[type]}`; + toast.innerHTML = ` + ${type === 'success' ? '✓' : type === 'error' ? '✕' : '!'} + ${message} + `; + + container.appendChild(toast); + setTimeout(() => { + toast.classList.remove('translate-y-10', 'opacity-0'); + }, 10); + + setTimeout(() => { + toast.classList.add('opacity-0', '-translate-y-10'); + setTimeout(() => toast.remove(), 500); + }, 4000); + } + function logout() { localStorage.removeItem('access_token'); localStorage.removeItem('user_role'); @@ -166,6 +207,9 @@ else if (page === 'companies') await renderCompanies(); else if (page === 'invoices') await renderInvoices(); else if (page === 'users') await renderUsers(); + else if (page === 'risk-monitor') await renderRiskMonitor(); + else if (page === 'settings') await renderSettings(); + else if (page === 'admin') await renderAdminStats(); } // ── Users View ─────────────────────────────────────────── @@ -733,6 +777,251 @@ } } + // ── Risk Monitor View ──────────────────────────────────── + async function renderRiskMonitor() { + document.getElementById('page-title').textContent = 'مراقبة المخاطر والالتزام'; + try { + contentDiv.innerHTML = ` +
+
+

+ + تحليل المخاطر الحالي +

+
+
+
+ مؤشر الالتزام العام + 94% +
+
+
+
+
+
+
+

فواتير مرفوضة

+

2

+
+
+

تنبيهات حرجة

+

0

+
+
+
+
+
+

عوامل الخطورة المكتشفة

+
+
+ +
+

تأخير في الرفع

+

تم رصد 5 فواتير تم رفعها بعد أكثر من 3 أيام من تاريخ الإصدار.

+
+
+
+ +
+

دقة البيانات

+

نسبة تطابق البيانات المستخرجة مع القواعد الضريبية بلغت 100% هذا الشهر.

+
+
+
+
+
+ `; + } catch (err) { + contentDiv.innerHTML = `
خطأ في تحميل بيانات المخاطر
`; + } + } + + // ── Settings View ──────────────────────────────────────── + async function renderSettings() { + document.getElementById('page-title').textContent = 'الإعدادات الشخصية والأمان'; + + contentDiv.innerHTML = ` +
+ +
+
+
+

التحقق بخطوتين (2FA)

+

أضف طبقة حماية إضافية لحسابك باستخدام تطبيق التحقق.

+
+
+ جاري التحقق... +
+
+
+ +
+
+ + +
+
+
+

مفاتيح API

+

استخدم هذه المفاتيح للربط مع تطبيقات خارجية أو الموبايل.

+
+ +
+
+ +
+
+
+ `; + + update2FAStatus(); + loadApiKeys(); + } + + async function update2FAStatus() { + try { + const res = await API.get('/auth/me'); + const user = res.data; + const statusEl = document.getElementById('2fa-status'); + const contentEl = document.getElementById('2fa-content'); + + if (user.totp_enabled) { + statusEl.innerHTML = '● مفعل'; + contentEl.innerHTML = ` +

حسابك محمي حالياً بالتحقق الثنائي.

+ + `; + } else { + statusEl.innerHTML = '○ غير مفعل'; + contentEl.innerHTML = ` +

ننصح بتفعيل التحقق الثنائي لحماية بياناتك المالية.

+ + `; + } + } catch (err) { console.error(err); } + } + + async function start2FASetup() { + const contentEl = document.getElementById('2fa-content'); + contentEl.innerHTML = '
جاري إنشاء رمز الأمان...
'; + + try { + const res = await API.post('/auth/2fa/enable', {}); + const { secret, qr_url } = res.data; + + contentEl.innerHTML = ` +
+
+ QR Code +
+
+

1. قم بمسح الرمز أعلاه باستخدام تطبيق Google Authenticator أو ما يماثله.

+

2. أدخل الرمز المكون من 6 أرقام للتأكيد:

+
+ + +
+

رمز الأمان اليدوي: ${secret}

+
+
+ `; + } catch (err) { + alert('فشل تفعيل التحقق الثنائي'); + update2FAStatus(); + } + } + + async function verify2FA(secret) { + const code = document.getElementById('2fa-code').value; + if (code.length !== 6) return; + + try { + await API.post('/auth/2fa/verify', { secret, code }); + alert('تم التفعيل بنجاح!'); + update2FAStatus(); + } catch (err) { + alert(err.error?.message_ar || 'الرمز غير صحيح'); + } + } + + async function disable2FA() { + if (!confirm('هل أنت متأكد من تعطيل التحقق الثنائي؟ سيقل أمان حسابك بشكل ملحوظ.')) return; + try { + await API.post('/auth/2fa/disable', {}); + update2FAStatus(); + } catch (err) { alert('فشل التعطيل'); } + } + + async function loadApiKeys() { + const listEl = document.getElementById('api-keys-list'); + try { + const res = await API.get('/api-keys'); + const keys = res.data; + + if (keys.length === 0) { + listEl.innerHTML = '

لا توجد مفاتيح API حالياً.

'; + return; + } + + listEl.innerHTML = keys.map(k => ` +
+
+

${k.name}

+

Prefix: ${k.prefix} • Created: ${new Date(k.created_at).toLocaleDateString()}

+
+
+ Active +
+
+ `).join(''); + } catch (err) { listEl.innerHTML = '

خطأ في تحميل المفاتيح

'; } + } + + async function showCreateApiKeyModal() { + const name = prompt('أدخل اسماً لهذا المفتاح (مثلاً: Flutter App):'); + if (!name) return; + try { + const res = await API.post('/api-keys', { name }); + alert(`تم إنشاء المفتاح بنجاح!\n\nمهم جداً: هذا هو المفتاح السري، قم بحفظه الآن لأنه لن يظهر مرة أخرى:\n\n${res.data.key}`); + loadApiKeys(); + } catch (err) { alert('فشل إنشاء المفتاح'); } + } + + // ── Admin Stats View ───────────────────────────────────── + async function renderAdminStats() { + document.getElementById('page-title').textContent = 'إدارة المنصة (Super Admin)'; + try { + const res = await API.get('/admin/stats'); + const stats = res.data; + + contentDiv.innerHTML = ` +
+
+

إجمالي المستأجرين

+

${stats.total_tenants}

+
+
+

إجمالي الفواتير

+

${stats.total_invoices}

+
+
+

حالة النظام

+

+ Redis: ${stats.system_health.redis.toUpperCase()} +

+
+
+ +
+

إدارة المستأجرين (قريباً)

+

سيتم عرض قائمة الشركات وإحصائيات الاستهلاك لكل منها هنا.

+
+ `; + } catch (err) { + contentDiv.innerHTML = `
عذراً، هذه الصفحة للمشرفين فقط.
`; + } + } + // ── Modals & Actions ───────────────────────────────────── function showAddCompanyModal() { const modals = document.getElementById('modals'); @@ -898,13 +1187,15 @@ document.getElementById('header').classList.add('flex'); // Hide Users menu if not admin or super_admin + // Nav link visibility based on roles const role = localStorage.getItem('user_role'); const usersNav = document.getElementById('nav-users'); - if (role !== 'super_admin' && role !== 'admin') { - if (usersNav) usersNav.style.display = 'none'; - } else { - if (usersNav) usersNav.style.display = 'flex'; - } + const adminNav = document.getElementById('nav-admin'); + const riskNav = document.getElementById('nav-risk-monitor'); + + if (usersNav) usersNav.style.display = (role === 'super_admin' || role === 'admin') ? 'flex' : 'none'; + if (adminNav) adminNav.style.display = (role === 'super_admin') ? 'flex' : 'none'; + if (riskNav) riskNav.style.display = (role !== 'employee') ? 'flex' : 'none'; // AI Chat Listener document.getElementById('ai-query').onkeydown = async (e) => { @@ -923,6 +1214,7 @@ }; navigateTo('dashboard'); + showToast(`مرحباً بك مجدداً، ${localStorage.getItem('user_name') || 'مدير النظام'}`); } else { renderLogin(); } diff --git a/tests/Feature/AuthTest.php b/tests/Feature/AuthTest.php new file mode 100644 index 0000000..060f00a --- /dev/null +++ b/tests/Feature/AuthTest.php @@ -0,0 +1,19 @@ +assertTrue(true); + } +} diff --git a/tests/Unit/HmacTest.php b/tests/Unit/HmacTest.php new file mode 100644 index 0000000..3212156 --- /dev/null +++ b/tests/Unit/HmacTest.php @@ -0,0 +1,44 @@ +service = new HmacService(); + } + + public function test_it_verifies_valid_signature(): void + { + $secret = 'test-secret'; + $nonce = 'nonce-123'; + $timestamp = (string)time(); + $payload = json_encode(['foo' => 'bar']); + + $signature = $this->service->sign($payload, $secret, $nonce, $timestamp); + + $this->assertTrue($this->service->verify($payload, $signature, $secret, $nonce, $timestamp)); + } + + public function test_it_rejects_tampered_payload(): void + { + $secret = 'test-secret'; + $nonce = 'nonce-123'; + $timestamp = (string)time(); + $payload = json_encode(['foo' => 'bar']); + + $signature = $this->service->sign($payload, $secret, $nonce, $timestamp); + + $tamperedPayload = json_encode(['foo' => 'baz']); + + $this->assertFalse($this->service->verify($tamperedPayload, $signature, $secret, $nonce, $timestamp)); + } +} diff --git a/tests/Unit/TaxValidationTest.php b/tests/Unit/TaxValidationTest.php new file mode 100644 index 0000000..96034f3 --- /dev/null +++ b/tests/Unit/TaxValidationTest.php @@ -0,0 +1,51 @@ +service = new TaxValidationService(); + } + + public function test_it_validates_standard_invoice(): void + { + $data = [ + 'invoice_type' => '001', // Standard + 'total_tax_exclusive_amount' => 100, + 'total_tax_amount' => 16, + 'grand_total' => 116, + 'tax_items' => [ + ['tax_percent' => 16, 'tax_amount' => 16, 'taxable_amount' => 100] + ] + ]; + + $result = $this->service->validate($data); + $this->assertTrue($result['is_valid']); + } + + public function test_it_detects_mismatching_totals(): void + { + $data = [ + 'invoice_type' => '001', + 'total_tax_exclusive_amount' => 100, + 'total_tax_amount' => 16, + 'grand_total' => 110, // Error: should be 116 + 'tax_items' => [ + ['tax_percent' => 16, 'tax_amount' => 16, 'taxable_amount' => 100] + ] + ]; + + $result = $this->service->validate($data); + $this->assertFalse($result['is_valid']); + $this->assertContains('Grand total mismatch', $result['errors']); + } +}