feat: migrate WAHA configuration from system-wide to per-user settings with API key support

This commit is contained in:
root
2026-03-29 17:11:05 +07:00
parent dbe02e52fd
commit afb7ef2a37
2 changed files with 146 additions and 108 deletions

36
db.php
View File

@@ -56,21 +56,23 @@ function notify_group($group_id, $message) {
function notify_waha($action, $module, $item_name) { function notify_waha($action, $module, $item_name) {
global $pdo; global $pdo;
try { try {
$u_id = $_SESSION['user_id'] ?? 0; // Fetch all users with WhatsApp channel enabled and WAHA configured
$stmt = $pdo->query("SELECT id, username, waha_endpoint, waha_chat_id, waha_template, waha_session, waha_api_key FROM users WHERE notify_channels LIKE '%whatsapp%' AND waha_endpoint IS NOT NULL AND waha_endpoint != ''");
$wa_users = $stmt->fetchAll();
if (empty($wa_users)) return;
$current_user_name = "System"; $current_user_name = "System";
if($u_id) { if (isset($_SESSION['user_id'])) {
$u = get_user($u_id); $u = get_user($_SESSION['user_id']);
if($u) $current_user_name = $u['username']; if ($u) $current_user_name = $u['username'];
} }
$stmt = $pdo->query("SELECT waha_endpoint, waha_chat_id, waha_template FROM users WHERE notify_channels LIKE '%whatsapp%' AND waha_endpoint IS NOT NULL AND waha_endpoint != ''"); foreach ($wa_users as $wu) {
while ($user = $stmt->fetch()) { $endpoint = $wu['waha_endpoint'];
$endpoint = $user['waha_endpoint']; $chat_id = $wu['waha_chat_id'];
$chat_id = $user['waha_chat_id']; if (!$endpoint || !$chat_id) continue;
$template = $user['waha_template'] ?: "*Update:* {action} on {module} '{item}' by {user}";
if(!$endpoint || !$chat_id) continue;
$template = $wu['waha_template'] ?: "*Update:* {action} on {module} '{item}' by {user}";
$text = str_replace( $text = str_replace(
['{action}', '{module}', '{item}', '{user}'], ['{action}', '{module}', '{item}', '{user}'],
[$action, $module, $item_name, $current_user_name], [$action, $module, $item_name, $current_user_name],
@@ -80,17 +82,21 @@ function notify_waha($action, $module, $item_name) {
$payload = json_encode([ $payload = json_encode([
"chatId" => $chat_id, "chatId" => $chat_id,
"text" => $text, "text" => $text,
"session" => "default" "session" => $wu['waha_session'] ?: 'default'
]); ]);
$ch = curl_init($endpoint); $ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); $headers = ['Content-Type: application/json'];
curl_setopt($ch, CURLOPT_TIMEOUT, 1); if (!empty($wu['waha_api_key'])) {
$headers[] = 'X-Api-Key: ' . $wu['waha_api_key'];
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 2);
curl_exec($ch); curl_exec($ch);
curl_close($ch); curl_close($ch);
} }
} catch(Exception $e) {} } catch (Exception $e) {}
} }

View File

@@ -3,13 +3,15 @@ if(empty($_SESSION['is_admin'])) die("Access Denied");
// ── DB Migrations ───────────────────────────────────────────────────────────── // ── DB Migrations ─────────────────────────────────────────────────────────────
try { $pdo->exec("ALTER TABLE users ADD COLUMN profile_pic TEXT DEFAULT NULL"); } catch(Exception $e) {} try { $pdo->exec("ALTER TABLE users ADD COLUMN profile_pic TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE users ADD COLUMN waha_endpoint TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE users ADD COLUMN waha_chat_id TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE users ADD COLUMN waha_template TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE users ADD COLUMN waha_session TEXT DEFAULT 'default'"); } catch(Exception $e) {}
try { $pdo->exec("ALTER TABLE users ADD COLUMN waha_api_key TEXT DEFAULT NULL"); } catch(Exception $e) {}
try { try {
$pdo->exec("CREATE TABLE IF NOT EXISTS system_config (k TEXT PRIMARY KEY, v TEXT)"); $pdo->exec("CREATE TABLE IF NOT EXISTS system_config (k TEXT PRIMARY KEY, v TEXT)");
$pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('completed_days', '7')"); $pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('completed_days', '7')");
$pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('archived_days', '90')"); $pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('archived_days', '90')");
$pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('waha_endpoint', '')");
$pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('waha_chat_id', '')");
$pdo->exec("INSERT OR IGNORE INTO system_config (k, v) VALUES ('waha_template', '')");
} catch(Exception $e) {} } catch(Exception $e) {}
// ── Handle POST ─────────────────────────────────────────────────────────────── // ── Handle POST ───────────────────────────────────────────────────────────────
@@ -79,8 +81,13 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
if(isset($_POST['update_user_channels'])) { if(isset($_POST['update_user_channels'])) {
$ch = $_POST['channels'] ?? []; $ch = $_POST['channels'] ?? [];
$ch_str = is_array($ch) ? implode(',', $ch) : ''; $ch_str = is_array($ch) ? implode(',', $ch) : '';
$stmt = $pdo->prepare("UPDATE users SET notify_channels=?, telegram_id=?, email=?, whatsapp_number=? WHERE id=?"); $waha_ep = trim($_POST['waha_endpoint'] ?? '');
$stmt->execute([$ch_str, $_POST['telegram_id'], $_POST['email'], $_POST['whatsapp_number'], $_POST['user_id']]); $waha_cid = trim($_POST['waha_chat_id'] ?? '');
$waha_tpl = trim($_POST['waha_template'] ?? '');
$waha_ses = trim($_POST['waha_session'] ?? '') ?: 'default';
$waha_key = trim($_POST['waha_api_key'] ?? '');
$stmt = $pdo->prepare("UPDATE users SET notify_channels=?, telegram_id=?, email=?, whatsapp_number=?, waha_endpoint=?, waha_chat_id=?, waha_template=?, waha_session=?, waha_api_key=? WHERE id=?");
$stmt->execute([$ch_str, $_POST['telegram_id'], $_POST['email'], $_POST['whatsapp_number'], $waha_ep ?: null, $waha_cid ?: null, $waha_tpl ?: null, $waha_ses, $waha_key ?: null, $_POST['user_id']]);
$success_msg = "User notification preferences updated!"; $success_msg = "User notification preferences updated!";
} }
@@ -88,15 +95,12 @@ if($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = $pdo->prepare("UPDATE system_config SET v=? WHERE k=?"); $stmt = $pdo->prepare("UPDATE system_config SET v=? WHERE k=?");
$stmt->execute([$_POST['completed_days'] ?? 7, 'completed_days']); $stmt->execute([$_POST['completed_days'] ?? 7, 'completed_days']);
$stmt->execute([$_POST['archived_days'] ?? 90, 'archived_days']); $stmt->execute([$_POST['archived_days'] ?? 90, 'archived_days']);
$stmt->execute([$_POST['waha_endpoint'] ?? '', 'waha_endpoint']);
$stmt->execute([$_POST['waha_chat_id'] ?? '', 'waha_chat_id']);
$stmt->execute([$_POST['waha_template'] ?? '', 'waha_template']);
$success_msg = "System configuration saved!"; $success_msg = "System configuration saved!";
} }
} }
// ── Fetch Data ──────────────────────────────────────────────────────────────── // ── Fetch Data ────────────────────────────────────────────────────────────────
$users = query("SELECT id, username, is_admin, notify_channels, telegram_id, email, whatsapp_number, profile_pic FROM users ORDER BY is_admin DESC, username ASC")->fetchAll(); $users = query("SELECT id, username, is_admin, notify_channels, telegram_id, email, whatsapp_number, profile_pic, waha_endpoint, waha_chat_id, waha_template, waha_session, waha_api_key FROM users ORDER BY is_admin DESC, username ASC")->fetchAll();
$all_groups = query("SELECT * FROM groups ORDER BY name")->fetchAll(); $all_groups = query("SELECT * FROM groups ORDER BY name")->fetchAll();
$conf_comp = query("SELECT v FROM system_config WHERE k='completed_days'")->fetchColumn() ?: 7; $conf_comp = query("SELECT v FROM system_config WHERE k='completed_days'")->fetchColumn() ?: 7;
$conf_arch = query("SELECT v FROM system_config WHERE k='archived_days'")->fetchColumn() ?: 90; $conf_arch = query("SELECT v FROM system_config WHERE k='archived_days'")->fetchColumn() ?: 90;
@@ -368,60 +372,100 @@ $active_tab = $_GET['tab'] ?? 'overview';
<div class="admin-tab-pane <?= $active_tab==='channels'?'active':'' ?>" id="tab-channels"> <div class="admin-tab-pane <?= $active_tab==='channels'?'active':'' ?>" id="tab-channels">
<div class="card"> <div class="card">
<h3 style="font-size:1rem; font-weight:700; margin-bottom:0.5rem;"><i class="fa fa-bell"></i> User Notification Channels</h3> <h3 style="font-size:1rem; font-weight:700; margin-bottom:0.5rem;"><i class="fa fa-bell"></i> User Notification Channels</h3>
<p style="font-size:0.85rem; color:var(--text-light); margin-bottom:1.25rem;">Configure notification channels (Native, Telegram, Email, WhatsApp) per user.</p> <p style="font-size:0.85rem; color:var(--text-light); margin-bottom:1.25rem;">Configure notification channels per user. WAHA settings appear when <strong>WhatsApp</strong> is enabled.</p>
<div style="overflow-x:auto;">
<table>
<thead>
<tr>
<th>User</th>
<th>Role</th>
<th>Channels</th>
<th>Telegram ID</th>
<th>Email</th>
<th>WhatsApp</th>
<th>Save</th>
</tr>
</thead>
<tbody>
<?php foreach($users as $u): <?php foreach($users as $u):
$chs = explode(',', $u['notify_channels'] ?? ''); $chs = explode(',', $u['notify_channels'] ?? '');
$uid = $u['id'];
$hasWA = in_array('whatsapp', $chs);
?> ?>
<tr> <div class="card" style="margin-bottom:1rem; padding:1rem; border:1px solid var(--border);">
<form method="POST"> <form method="POST">
<input type="hidden" name="user_id" value="<?= $u['id'] ?>"> <input type="hidden" name="user_id" value="<?= $uid ?>">
<td>
<div style="display:flex; align-items:center; gap:0.5rem;"> <!-- User header row -->
<div style="width:28px;height:28px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;flex-shrink:0;overflow:hidden;"> <div style="display:flex; align-items:center; gap:0.75rem; flex-wrap:wrap;">
<!-- Avatar -->
<div style="width:36px;height:36px;border-radius:50%;background:var(--primary);color:#fff;display:flex;align-items:center;justify-content:center;font-size:0.85rem;font-weight:700;flex-shrink:0;overflow:hidden;">
<?php if(($u['profile_pic']??null) && file_exists($u['profile_pic'])): ?> <?php if(($u['profile_pic']??null) && file_exists($u['profile_pic'])): ?>
<img src="<?= htmlspecialchars($u['profile_pic']) ?>" style="width:100%;height:100%;object-fit:cover;"> <img src="<?= htmlspecialchars($u['profile_pic']) ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?> <?php else: ?>
<?= strtoupper(mb_substr($u['username'],0,1)) ?> <?= strtoupper(mb_substr($u['username'],0,1)) ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
<strong><?= htmlspecialchars($u['username']) ?></strong>
<!-- Name & role -->
<div style="flex:1;">
<div style="font-weight:700; font-size:0.95rem;"><?= htmlspecialchars($u['username']) ?></div>
<div><span class="<?= $u['is_admin']?'admin-badge-admin':'admin-badge-user' ?>" style="font-size:0.7rem;"><?= $u['is_admin']?'Admin':'User' ?></span></div>
</div> </div>
</td>
<td><span class="<?= $u['is_admin']?'admin-badge-admin':'admin-badge-user' ?>"><?= $u['is_admin']?'Admin':'User' ?></span></td> <!-- Save button -->
<td> <button type="submit" name="update_user_channels" class="btn btn-sm"><i class="fa fa-save"></i> Save</button>
<div style="display:flex; gap:0.5rem; flex-wrap:wrap;"> </div>
<!-- Channel checkboxes -->
<div style="display:flex; gap:1rem; flex-wrap:wrap; margin-top:0.85rem; padding-top:0.75rem; border-top:1px dashed var(--border);">
<?php foreach(['native','email','telegram','whatsapp'] as $ch): ?> <?php foreach(['native','email','telegram','whatsapp'] as $ch): ?>
<label style="display:flex; align-items:center; gap:3px; font-size:0.8rem; cursor:pointer; white-space:nowrap;"> <label style="display:flex; align-items:center; gap:5px; font-size:0.85rem; cursor:pointer; font-weight:500;">
<input type="checkbox" name="channels[]" value="<?= $ch ?>" <?= in_array($ch,$chs)?'checked':'' ?>> <input type="checkbox" name="channels[]" value="<?= $ch ?>"
<?= ucfirst($ch) ?> <?= in_array($ch,$chs)?'checked':'' ?>
<?= $ch==='whatsapp' ? "onchange=\"toggleWAHA({$uid}, this.checked)\"" : '' ?>>
<?php
$icons = ['native'=>'🔔','email'=>'📧','telegram'=>'✈️','whatsapp'=>'📱'];
echo $icons[$ch] . ' ' . ucfirst($ch);
?>
</label> </label>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</td>
<td><input type="text" name="telegram_id" value="<?= htmlspecialchars($u['telegram_id']??'') ?>" class="form-control" style="width:100px; padding:0.35rem;"></td> <!-- Extra fields row -->
<td><input type="email" name="email" value="<?= htmlspecialchars($u['email']??'') ?>" class="form-control" style="width:160px; padding:0.35rem;"></td> <div style="display:flex; gap:1rem; flex-wrap:wrap; margin-top:0.75rem; align-items:flex-end;">
<td><input type="text" name="whatsapp_number" value="<?= htmlspecialchars($u['whatsapp_number']??'') ?>" class="form-control" style="width:120px; padding:0.35rem;"></td> <div class="form-group" style="margin:0; min-width:130px;">
<td><button type="submit" name="update_user_channels" class="btn btn-sm"><i class="fa fa-save"></i></button></td> <label style="font-size:0.75rem;">Telegram ID</label>
</form> <input type="text" name="telegram_id" value="<?= htmlspecialchars($u['telegram_id']??'') ?>" class="form-control" style="padding:0.35rem;" placeholder="@username or ID">
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div> </div>
<div class="form-group" style="margin:0; min-width:180px;">
<label style="font-size:0.75rem;">Email</label>
<input type="email" name="email" value="<?= htmlspecialchars($u['email']??'') ?>" class="form-control" style="padding:0.35rem;" placeholder="user@example.com">
</div>
<div class="form-group" style="margin:0; min-width:140px;">
<label style="font-size:0.75rem;">WhatsApp Number</label>
<input type="text" name="whatsapp_number" value="<?= htmlspecialchars($u['whatsapp_number']??'') ?>" class="form-control" style="padding:0.35rem;" placeholder="6281234567890">
</div>
</div>
<!-- WAHA expandable section -->
<div id="waha-section-<?= $uid ?>" style="<?= $hasWA ? '' : 'display:none;' ?> margin-top:0.75rem; padding:0.85rem; background:rgba(37,211,102,0.06); border:1px solid rgba(37,211,102,0.25); border-radius:8px;">
<div style="font-size:0.78rem; font-weight:700; color:#25D366; margin-bottom:0.65rem;"><i class="fab fa-whatsapp"></i> WAHA WhatsApp API Settings</div>
<div style="display:flex; gap:1rem; flex-wrap:wrap; align-items:flex-end;">
<div class="form-group" style="margin:0; flex:2; min-width:200px;">
<label style="font-size:0.75rem;">WAHA Endpoint URL</label>
<input type="text" name="waha_endpoint" value="<?= htmlspecialchars($u['waha_endpoint']??'') ?>" class="form-control" style="padding:0.35rem;" placeholder="http://localhost:3000/api/sendText">
</div>
<div class="form-group" style="margin:0; flex:1; min-width:120px;">
<label style="font-size:0.75rem;">Session Name</label>
<input type="text" name="waha_session" value="<?= htmlspecialchars($u['waha_session'] ?? 'default') ?>" class="form-control" style="padding:0.35rem;" placeholder="default">
</div>
<div class="form-group" style="margin:0; flex:1; min-width:160px;">
<label style="font-size:0.75rem;">Chat ID</label>
<input type="text" name="waha_chat_id" value="<?= htmlspecialchars($u['waha_chat_id']??'') ?>" class="form-control" style="padding:0.35rem;" placeholder="6281234567890@c.us">
</div>
<div class="form-group" style="margin:0; flex:1; min-width:160px;">
<label style="font-size:0.75rem;">API Key <span style="font-weight:400; color:var(--text-light);">(X-Api-Key)</span></label>
<input type="password" name="waha_api_key" value="<?= htmlspecialchars($u['waha_api_key']??'') ?>" class="form-control" style="padding:0.35rem; font-family:monospace;" placeholder="Leave blank if not required">
</div>
</div>
<div class="form-group" style="margin-top:0.6rem;">
<label style="font-size:0.75rem;">Message Template</label>
<textarea name="waha_template" class="form-control" rows="2" style="padding:0.35rem; font-size:0.8rem;" placeholder="*Alert:* {action} on {module} '{item}' by {user}"><?= htmlspecialchars($u['waha_template']??'') ?></textarea>
<div style="font-size:0.72rem; color:var(--text-light); margin-top:3px;">Variables: <code>{action}</code> <code>{module}</code> <code>{item}</code> <code>{user}</code></div>
</div>
</div>
</form>
</div>
<?php endforeach; ?>
</div> </div>
</div> </div>
@@ -445,31 +489,6 @@ $active_tab = $_GET['tab'] ?? 'overview';
</div> </div>
<button type="submit" name="update_sys_config" class="btn" style="margin-top:1.25rem;"><i class="fa fa-save"></i> Save Configuration</button> <button type="submit" name="update_sys_config" class="btn" style="margin-top:1.25rem;"><i class="fa fa-save"></i> Save Configuration</button>
</form> </form>
<h3 style="font-size:1rem; font-weight:700; margin-bottom:0.5rem; margin-top:2rem;"><i class="fab fa-whatsapp"></i> WAHA WhatsApp Notifications</h3>
<p style="font-size:0.85rem; color:var(--text-light); margin-bottom:1.5rem;">Configure centralized WhatsApp notifications globally via WAHA (WhatsApp HTTP API).</p>
<form method="POST">
<input type="hidden" name="completed_days" value="<?= htmlspecialchars($conf_comp) ?>">
<input type="hidden" name="archived_days" value="<?= htmlspecialchars($conf_arch) ?>">
<div style="display:grid; grid-template-columns:1fr; gap:1.5rem; max-width:600px;">
<div class="form-group" style="margin:0;">
<label>WAHA Endpoint URL</label>
<input type="text" name="waha_endpoint" value="<?= htmlspecialchars($waha_endpoint) ?>" class="form-control" placeholder="http://localhost:3000/api/sendText">
<div style="font-size:0.75rem; color:var(--text-light); margin-top:4px;">The full /api/sendText HTTP API endpoint where WAHA is hosted. Leave blank to disable WhatsApp Notification feature.</div>
</div>
<div class="form-group" style="margin:0;">
<label>Default Chat ID</label>
<input type="text" name="waha_chat_id" value="<?= htmlspecialchars($waha_chat_id) ?>" class="form-control" placeholder="1234567890@c.us or group-id@g.us">
<div style="font-size:0.75rem; color:var(--text-light); margin-top:4px;">The centralized group or destination ID.</div>
</div>
<div class="form-group" style="margin:0;">
<label>Message Template</label>
<textarea name="waha_template" class="form-control" rows="3" placeholder="*System Alert:* {action} on {module} '{item}' by {user}"><?= htmlspecialchars($waha_template) ?></textarea>
<div style="font-size:0.75rem; color:var(--text-light); margin-top:4px;">Available Variables: <code>{action}</code>, <code>{module}</code>, <code>{item}</code>, <code>{user}</code></div>
</div>
</div>
<button type="submit" name="update_sys_config" class="btn" style="margin-top:1.25rem;"><i class="fa fa-save"></i> Save WAHA Configuration</button>
</form>
</div> </div>
<!-- Danger Zone --> <!-- Danger Zone -->
@@ -493,9 +512,22 @@ function switchTab(name) {
document.querySelectorAll('.admin-tab-pane').forEach(p => p.classList.remove('active')); document.querySelectorAll('.admin-tab-pane').forEach(p => p.classList.remove('active'));
document.querySelector(`[onclick="switchTab('${name}')"]`).classList.add('active'); document.querySelector(`[onclick="switchTab('${name}')"]`).classList.add('active');
document.getElementById('tab-'+name).classList.add('active'); document.getElementById('tab-'+name).classList.add('active');
// Update URL without reload
const url = new URL(window.location); const url = new URL(window.location);
url.searchParams.set('tab', name); url.searchParams.set('tab', name);
history.replaceState({}, '', url); history.replaceState({}, '', url);
} }
function toggleWAHA(uid, enabled) {
const section = document.getElementById('waha-section-' + uid);
if (!section) return;
if (enabled) {
section.style.display = 'block';
section.style.animation = 'fadeIn 0.25s ease';
} else {
section.style.display = 'none';
}
}
</script> </script>
<style>
@keyframes fadeIn { from { opacity:0; transform:translateY(-6px); } to { opacity:1; transform:none; } }
</style>