Deploy: 2026-05-22 02:16:46

This commit is contained in:
Hamza-Ayed
2026-05-22 02:16:46 +03:00
parent f793092a17
commit 479aedcbcf
4 changed files with 128 additions and 68 deletions

View File

@@ -43,6 +43,7 @@ class EndpointController extends BaseController
'endpoint_url' => $body['endpoint_url'],
'action_type' => $body['action_type'],
'description' => $body['description'] ?? null,
'api_key' => $body['api_key'] ?? null,
'headers' => $body['headers'] ?? null
];

View File

@@ -488,11 +488,17 @@ class WhatsAppController extends BaseController
]);
$headers = ['Content-Type: application/json'];
if ($endpoint && !empty($endpoint['headers'])) {
$customHeaders = json_decode($endpoint['headers'], true);
if (is_array($customHeaders)) {
foreach ($customHeaders as $key => $value) {
$headers[] = "$key: $value";
if ($endpoint) {
if (!empty($endpoint['api_key'])) {
$headers[] = 'X-API-Key: ' . $endpoint['api_key'];
$headers[] = 'Authorization: Bearer ' . $endpoint['api_key'];
}
if (!empty($endpoint['headers'])) {
$customHeaders = json_decode($endpoint['headers'], true);
if (is_array($customHeaders)) {
foreach ($customHeaders as $key => $value) {
$headers[] = "$key: $value";
}
}
}
} else {
@@ -540,6 +546,10 @@ class WhatsAppController extends BaseController
]);
$headers = ['Content-Type: application/json'];
if (!empty($endpoint['api_key'])) {
$headers[] = 'X-API-Key: ' . $endpoint['api_key'];
$headers[] = 'Authorization: Bearer ' . $endpoint['api_key'];
}
if (!empty($endpoint['headers'])) {
$customHeaders = json_decode($endpoint['headers'], true);
if (is_array($customHeaders)) {

View File

@@ -69,6 +69,7 @@ class CompanyEndpoint extends BaseModel
`endpoint_url` VARCHAR(512) NOT NULL,
`action_type` VARCHAR(100) NOT NULL,
`description` TEXT NULL,
`api_key` VARCHAR(255) NULL,
`headers` TEXT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
@@ -76,6 +77,14 @@ class CompanyEndpoint extends BaseModel
INDEX `idx_endpoint_action` (`action_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
// Auto-migration: check if api_key column exists, if not, add it
try {
Database::execute("SELECT `api_key` FROM `company_endpoints` LIMIT 1");
} catch (\Exception $colException) {
Database::execute("ALTER TABLE `company_endpoints` ADD COLUMN `api_key` VARCHAR(255) NULL AFTER `description`");
}
$checked = true;
} catch (\Exception $e) {
error_log("Failed to ensure company_endpoints table: " . $e->getMessage());

View File

@@ -627,9 +627,37 @@
.recording-pulse {
animation: pulse-red 1.2s infinite;
}
/* RTL Overrides */
body[dir="rtl"] .nav-item {
text-align: right;
flex-direction: row;
}
body[dir="rtl"] .data-table {
text-align: right;
}
body[dir="rtl"] .modal-header {
flex-direction: row-reverse;
}
body[dir="rtl"] .form-group {
text-align: right;
}
body[dir="rtl"] .form-label {
text-align: right;
display: block;
}
body[dir="rtl"] .dashboard-header {
flex-direction: row-reverse;
}
body[dir="rtl"] .user-info {
flex-direction: row-reverse;
}
body[dir="rtl"] .modal-footer {
flex-direction: row-reverse;
}
</style>
</head>
<body x-data="app()" x-init="checkAuth()">
<body x-data="app()" x-init="checkAuth()" :dir="lang === 'ar' ? 'rtl' : 'ltr'">
<div class="app-container">
<!-- Auth View -->
@@ -708,17 +736,23 @@
<!-- Main Dashboard View -->
<template x-if="isLoggedIn">
<div>
<div class="dashboard-header" style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 16px;">
<div class="dashboard-header" style="display: flex; align-items: center; justify-content: space-between;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<div style="display: flex; align-items: center; gap: 16px;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<img src="/logo.svg" alt="Nabeh Logo" style="width: 52px; height: 52px; border-radius: 12px; box-shadow: 0 0 15px rgba(6, 182, 212, 0.35); border: 1px solid rgba(255, 255, 255, 0.08);">
<div>
<h1 style="font-size: 1.8rem; margin: 0 0 0.15rem 0;">Nabeh Dashboard</h1>
<p class="text-muted" style="font-size: 0.9rem; margin: 0;">Connected to system: <span class="font-semibold" x-text="user.name"></span></p>
<div :style="lang === 'ar' ? 'text-align: right;' : ''">
<h1 style="font-size: 1.8rem; margin: 0 0 0.15rem 0;" x-text="lang === 'ar' ? 'لوحة تحكم نبيه' : 'Nabeh Dashboard'"></h1>
<p class="text-muted" style="font-size: 0.9rem; margin: 0;">
<span x-text="lang === 'ar' ? 'متصل بالنظام باسم:' : 'Connected to system:'"></span>
<span class="font-semibold" x-text="user.name"></span>
</p>
</div>
</div>
<div class="user-info">
<div class="user-info" style="display: flex; align-items: center; gap: 12px;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<button @click="toggleLang()" class="btn btn-secondary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" id="lang-toggle-btn">
<span x-text="lang === 'ar' ? 'English 🇬🇧' : 'العربية 🇸🇦'"></span>
</button>
<div class="avatar" x-text="user.name.charAt(0).toUpperCase()"></div>
<button @click="logout()" class="btn btn-secondary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" id="logout-btn">Log Out</button>
<button @click="logout()" class="btn btn-secondary" style="width: auto; padding: 0.5rem 1rem; font-size: 0.85rem;" id="logout-btn" x-text="lang === 'ar' ? 'تسجيل الخروج' : 'Log Out'"></button>
</div>
</div>
@@ -726,22 +760,22 @@
<!-- Left Sidebar Nav -->
<div class="nav-menu">
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'whatsapp' }" @click="activeDashboardTab = 'whatsapp'" id="nav-whatsapp-btn">
<span>📱</span> WhatsApp Connection
<span>📱</span> <span x-text="lang === 'ar' ? 'اتصال الواتساب' : 'WhatsApp Connection'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'contacts' }" @click="activeDashboardTab = 'contacts'; fetchContacts(); fetchGroups()" id="nav-contacts-btn">
<span>👥</span> Contacts Directory
<span>👥</span> <span x-text="lang === 'ar' ? 'دليل جهات الاتصال' : 'Contacts Directory'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'templates' }" @click="activeDashboardTab = 'templates'; fetchTemplates()" id="nav-templates-btn">
<span>📝</span> Message Templates
<span>📝</span> <span x-text="lang === 'ar' ? 'قوالب الرسائل' : 'Message Templates'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'campaigns' }" @click="activeDashboardTab = 'campaigns'; fetchCampaigns()" id="nav-campaigns-btn">
<span>📣</span> Marketing Campaigns
<span>📣</span> <span x-text="lang === 'ar' ? 'الحملات التسويقية' : 'Marketing Campaigns'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'chatbot' }" @click="activeDashboardTab = 'chatbot'; fetchChatbotSettings()" id="nav-chatbot-btn">
<span>🤖</span> AI Chatbot Settings
<span>🤖</span> <span x-text="lang === 'ar' ? 'روبوت الذكاء الاصطناعي' : 'AI Chatbot Settings'"></span>
</button>
<button class="nav-item" :class="{ 'active': activeDashboardTab === 'integrations' }" @click="activeDashboardTab = 'integrations'; fetchEndpoints()" id="nav-integrations-btn">
<span>🔌</span> API Integrations
<span>🔌</span> <span>ربط تطبيق نبيه بمشروعك (API Integrations)</span>
</button>
</div>
@@ -1069,26 +1103,25 @@
<!-- Panel: API Integrations -->
<div class="panel" x-show="activeDashboardTab === 'integrations'" id="panel-integrations">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 style="font-size: 1.4rem; margin: 0;">API Endpoints Integration</h2>
<button class="btn btn-primary" style="width: auto;" @click="endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', headers: '' }; showAddEndpointModal = true" id="add-endpoint-btn">
+ Add API Integration
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<h2 style="font-size: 1.4rem; margin: 0;" x-text="lang === 'ar' ? 'ربط تطبيق نبيه بمشروعك (API Integrations)' : 'API Endpoints Integration'"></h2>
<button class="btn btn-primary" style="width: auto;" @click="endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', api_key: '' }; showAddEndpointModal = true" id="add-endpoint-btn">
<span x-text="lang === 'ar' ? '+ إضافة ربط برمجي' : '+ Add API Integration'"></span>
</button>
</div>
<p class="text-muted" style="margin-bottom: 1.5rem; font-size: 0.9rem;">
Configure external web APIs for multi-tenant integrations. The chatbot can fetch user profiles or verify payment details dynamically.
<p class="text-muted" style="margin-bottom: 1.5rem; font-size: 0.9rem;" x-text="lang === 'ar' ? 'قم بتهيئة واجهات برمجة التطبيقات الخارجية (Web APIs) للربط البرمجي بمشروعك. يمكن للروبوت جلب الملفات التعريفية للمستخدمين أو التحقق من تفاصيل الدفع ديناميكيًا.' : 'Configure external web APIs for multi-tenant integrations. The chatbot can fetch user profiles or verify payment details dynamically.'">
</p>
<div class="data-table-container">
<table class="data-table">
<thead>
<tr>
<th>Integration Name</th>
<th>Action Type</th>
<th>Endpoint URL</th>
<th>Description</th>
<th style="width: 150px; text-align: center;">Actions</th>
<th x-text="lang === 'ar' ? 'اسم الربط' : 'Integration Name'"></th>
<th x-text="lang === 'ar' ? 'نوع الإجراء' : 'Action Type'"></th>
<th x-text="lang === 'ar' ? 'رابط نقطة النهاية (Endpoint URL)' : 'Endpoint URL'"></th>
<th x-text="lang === 'ar' ? 'الوصف' : 'Description'"></th>
<th style="width: 150px; text-align: center;" x-text="lang === 'ar' ? 'الخيارات' : 'Actions'"></th>
</tr>
</thead>
<tbody>
@@ -1096,20 +1129,20 @@
<tr>
<td class="font-semibold" x-text="endpoint.name"></td>
<td>
<span class="status-badge" :class="endpoint.action_type === 'verify_payment' ? 'badge-waiting_qr' : 'badge-connecting'" style="margin: 0; padding: 0.2rem 0.5rem; font-size: 0.75rem;" x-text="endpoint.action_type === 'verify_payment' ? 'Verify Payment' : 'Fetch User Info'"></span>
<span class="status-badge" :class="endpoint.action_type === 'verify_payment' ? 'badge-waiting_qr' : 'badge-connecting'" style="margin: 0; padding: 0.2rem 0.5rem; font-size: 0.75rem;" x-text="endpoint.action_type === 'verify_payment' ? (lang === 'ar' ? 'تأكيد الدفع' : 'Verify Payment') : (endpoint.action_type === 'fetch_user_info' ? (lang === 'ar' ? 'جلب بيانات العميل' : 'Fetch User Info') : endpoint.action_type)"></span>
</td>
<td style="font-family: monospace; font-size: 0.85rem;" x-text="endpoint.endpoint_url"></td>
<td x-text="endpoint.description || 'No description provided.'"></td>
<td style="text-align: center; display: flex; gap: 0.5rem; justify-content: center; align-items: center;">
<button class="btn btn-secondary" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="editEndpoint(endpoint)" :id="'edit-endpoint-btn-' + endpoint.id">Edit</button>
<button class="btn btn-danger" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="deleteEndpoint(endpoint.id)" :id="'delete-endpoint-btn-' + endpoint.id">Delete</button>
<td x-text="endpoint.description || (lang === 'ar' ? 'لا يوجد وصف.' : 'No description provided.')"></td>
<td style="text-align: center; display: flex; gap: 0.5rem; justify-content: center; align-items: center;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<button class="btn btn-secondary" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="editEndpoint(endpoint)" :id="'edit-endpoint-btn-' + endpoint.id" x-text="lang === 'ar' ? 'تعديل' : 'Edit'"></button>
<button class="btn btn-danger" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" @click="deleteEndpoint(endpoint.id)" :id="'delete-endpoint-btn-' + endpoint.id" x-text="lang === 'ar' ? 'حذف' : 'Delete'"></button>
</td>
</tr>
</template>
</tbody>
</table>
<template x-if="endpoints.length === 0">
<div class="empty-state" id="empty-endpoints-state">No API endpoints configured yet. Connect Intaleq or Salla integrations.</div>
<div class="empty-state" id="empty-endpoints-state" x-text="lang === 'ar' ? 'لم يتم تكوين نقاط نهاية واجهة برمجة تطبيقات (API Endpoints) بعد.' : 'No API endpoints configured yet. Connect Intaleq or Salla integrations.'"></div>
</template>
</div>
</div>
@@ -1157,43 +1190,49 @@
<!-- Modal: Add/Edit Endpoint Integration -->
<div class="modal-overlay" x-show="showAddEndpointModal" id="modal-add-endpoint" style="display: none;">
<div class="modal-card">
<div class="modal-header">
<h3 class="modal-title" x-text="endpointForm.id ? 'Edit API Endpoint Integration' : 'Add API Endpoint Integration'"></h3>
<div class="modal-header" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<h3 class="modal-title" x-text="endpointForm.id ? (lang === 'ar' ? 'تعديل ربط تطبيق نبيه بمشروعك' : 'Edit API Endpoint Integration') : (lang === 'ar' ? 'إضافة ربط تطبيق نبيه بمشروعك' : 'Add API Endpoint Integration')"></h3>
<button class="modal-close" @click="showAddEndpointModal = false">&times;</button>
</div>
<form @submit.prevent="submitAddEndpoint()" id="form-add-endpoint">
<div class="modal-body">
<div class="form-group">
<label class="form-label">Integration Name</label>
<label class="form-label" x-text="lang === 'ar' ? 'اسم الربط' : 'Integration Name'"></label>
<input type="text" class="form-input" x-model="endpointForm.name" required placeholder="e.g. Intaleq Driver Lookup" id="add-endpoint-name">
</div>
<div class="form-group">
<label class="form-label">Endpoint URL</label>
<label class="form-label" x-text="lang === 'ar' ? 'رابط نقطة النهاية (Endpoint URL)' : 'Endpoint URL'"></label>
<input type="url" class="form-input" x-model="endpointForm.endpoint_url" required placeholder="https://yourdomain.com/api/nabeh/action" id="add-endpoint-url">
</div>
<div class="form-group">
<label class="form-label">Action Type</label>
<select class="form-input" x-model="endpointForm.action_type" required id="add-endpoint-action-type">
<option value="verify_payment">Verify Payment (تحليل وتأكيد الدفع)</option>
<option value="fetch_user_info">Fetch User Info (جلب معلومات السائق أو العميل)</option>
</select>
<label class="form-label" x-text="lang === 'ar' ? 'نوع الإجراء (Action Type)' : 'Action Type'"></label>
<input type="text" class="form-input" x-model="endpointForm.action_type" required placeholder="e.g. verify_payment" id="add-endpoint-action-type">
<!-- Shortcuts/Hint Tags -->
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem; flex-wrap: wrap;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<button type="button" class="btn btn-secondary" style="width: auto; padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 4px;" @click="endpointForm.action_type = 'verify_payment'">
verify_payment (تحليل وتأكيد الدفع)
</button>
<button type="button" class="btn btn-secondary" style="width: auto; padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 4px;" @click="endpointForm.action_type = 'fetch_user_info'">
fetch_user_info (جلب معلومات العميل)
</button>
</div>
<span style="font-size: 0.75rem; color: var(--text-secondary); display: block; margin-top: 0.25rem;" x-text="lang === 'ar' ? 'يمكنك كتابة نوع إجراء مخصص هنا لتسهيل ربط النظام أو الضغط على الاختصارات بالأعلى.' : 'You can type a custom action type here or click the shortcuts above.'"></span>
</div>
<div class="form-group">
<label class="form-label">Description</label>
<label class="form-label" x-text="lang === 'ar' ? 'الوصف' : 'Description'"></label>
<textarea class="form-input" x-model="endpointForm.description" placeholder="A brief explanation of what this endpoint does..." id="add-endpoint-description"></textarea>
</div>
<div class="form-group">
<label class="form-label">Custom HTTP Headers (JSON Object)</label>
<textarea class="form-input" style="font-family: monospace; font-size: 0.85rem; height: 120px;" x-model="endpointForm.headers" placeholder='{&#10; "Authorization": "Bearer YOUR_SECRET_TOKEN",&#10; "X-Custom-Source": "Nabeh App"&#10;}' id="add-endpoint-headers"></textarea>
<span style="font-size: 0.75rem; color: var(--text-secondary); display: block; margin-top: 0.25rem;">
Optionally define any authorization tokens or custom headers in JSON format.
</span>
<label class="form-label" x-text="lang === 'ar' ? 'مفتاح واجهة برمجة التطبيقات (API Key)' : 'API Key'"></label>
<input type="password" class="form-input" x-model="endpointForm.api_key" placeholder="e.g. key_xxxxxxxxxxxxxxxxx" id="add-endpoint-api-key">
<span style="font-size: 0.75rem; color: var(--text-secondary); display: block; margin-top: 0.25rem;" x-text="lang === 'ar' ? 'سيتم إرسال هذا المفتاح في ترويسات X-API-Key و Authorization: Bearer عند استدعاء الخادم.' : 'This key will be automatically sent in X-API-Key and Authorization: Bearer headers for outbound server-to-server calls.'"></span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" style="width: auto;" @click="showAddEndpointModal = false">Cancel</button>
<div class="modal-footer" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<button type="button" class="btn btn-secondary" style="width: auto;" @click="showAddEndpointModal = false" x-text="lang === 'ar' ? 'إلغاء' : 'Cancel'"></button>
<button type="submit" class="btn btn-primary" style="width: auto;" :disabled="actionLoading">
<span x-show="!actionLoading" x-text="endpointForm.id ? 'Save Changes' : 'Create Integration'"></span>
<span x-show="!actionLoading" x-text="endpointForm.id ? (lang === 'ar' ? 'حفظ التعديلات' : 'Save Changes') : (lang === 'ar' ? 'إنشاء الربط' : 'Create Integration')"></span>
<span x-show="actionLoading" class="spinner"></span>
</button>
</div>
@@ -1305,6 +1344,9 @@
<script>
function app() {
return {
// Multi-Language State
lang: localStorage.getItem('nabeh_lang') || 'ar',
// Auth States
isLoggedIn: false,
authTab: 'login',
@@ -1344,7 +1386,7 @@
endpoint_url: '',
action_type: 'verify_payment',
description: '',
headers: ''
api_key: ''
},
endpoints: [],
@@ -1383,6 +1425,11 @@
},
// Methods
toggleLang() {
this.lang = this.lang === 'ar' ? 'en' : 'ar';
localStorage.setItem('nabeh_lang', this.lang);
},
checkAuth() {
this.token = localStorage.getItem('nabeh_token');
const storedUser = localStorage.getItem('nabeh_user');
@@ -1606,16 +1653,6 @@
async submitAddEndpoint() {
this.actionLoading = true;
try {
if (this.endpointForm.headers && this.endpointForm.headers.trim()) {
try {
JSON.parse(this.endpointForm.headers);
} catch (jsonErr) {
alert('Invalid JSON in Custom Headers. Please verify format.');
this.actionLoading = false;
return;
}
}
const response = await fetch('/api/endpoints', {
method: 'POST',
headers: {
@@ -1627,7 +1664,7 @@
const data = await response.json();
if (response.ok) {
this.showAddEndpointModal = false;
this.endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', headers: '' };
this.endpointForm = { id: null, name: '', endpoint_url: '', action_type: 'verify_payment', description: '', api_key: '' };
await this.fetchEndpoints();
} else {
alert(data.message || 'Failed to save integration endpoint.');
@@ -1646,13 +1683,16 @@
endpoint_url: endpoint.endpoint_url,
action_type: endpoint.action_type,
description: endpoint.description || '',
headers: endpoint.headers || ''
api_key: endpoint.api_key || ''
};
this.showAddEndpointModal = true;
},
async deleteEndpoint(id) {
if (!confirm('Are you sure you want to delete this integration endpoint?')) return;
const confirmMsg = this.lang === 'ar'
? 'هل أنت متأكد من رغبتك في حذف ربط نقطة النهاية هذا؟'
: 'Are you sure you want to delete this integration endpoint?';
if (!confirm(confirmMsg)) return;
this.actionLoading = true;
try {
const response = await fetch('/api/endpoints', {