Add test notification channel feature with AJAX support
Introduces the ability to test notification channels (email, Telegram, Discord, Slack) from the group edit page, both for new and existing channels. Adds a new testChannel method to NotificationGroupController with AJAX and form support, improves validation and error handling, and updates the UI to include test buttons and dynamic toast notifications. Also registers the new /channels/test route.
This commit is contained in:
@@ -68,6 +68,7 @@ class NotificationGroupController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$id = $this->groupModel->create([
|
$id = $this->groupModel->create([
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'description' => $description
|
'description' => $description
|
||||||
@@ -75,6 +76,14 @@ class NotificationGroupController extends Controller
|
|||||||
|
|
||||||
$_SESSION['success'] = "Group '$name' created successfully";
|
$_SESSION['success'] = "Group '$name' created successfully";
|
||||||
$this->redirect("/groups/edit?id=$id");
|
$this->redirect("/groups/edit?id=$id");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log the error using the ErrorHandler service
|
||||||
|
$errorHandler = new \App\Services\ErrorHandler();
|
||||||
|
$errorHandler->handleException($e);
|
||||||
|
|
||||||
|
$_SESSION['error'] = 'Failed to create notification group. Please try again.';
|
||||||
|
$this->redirect('/groups/create');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function edit()
|
public function edit()
|
||||||
@@ -129,6 +138,7 @@ class NotificationGroupController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$this->groupModel->update($id, [
|
$this->groupModel->update($id, [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'description' => $description
|
'description' => $description
|
||||||
@@ -136,6 +146,14 @@ class NotificationGroupController extends Controller
|
|||||||
|
|
||||||
$_SESSION['success'] = 'Group updated successfully';
|
$_SESSION['success'] = 'Group updated successfully';
|
||||||
$this->redirect("/groups/edit?id=$id");
|
$this->redirect("/groups/edit?id=$id");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log the error using the ErrorHandler service
|
||||||
|
$errorHandler = new \App\Services\ErrorHandler();
|
||||||
|
$errorHandler->handleException($e);
|
||||||
|
|
||||||
|
$_SESSION['error'] = 'Failed to update notification group. Please try again.';
|
||||||
|
$this->redirect("/groups/edit?id=$id");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete()
|
public function delete()
|
||||||
@@ -149,9 +167,18 @@ class NotificationGroupController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$this->groupModel->deleteWithRelations($id);
|
$this->groupModel->deleteWithRelations($id);
|
||||||
$_SESSION['success'] = 'Group deleted successfully';
|
$_SESSION['success'] = 'Group deleted successfully';
|
||||||
$this->redirect('/groups');
|
$this->redirect('/groups');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log the error using the ErrorHandler service
|
||||||
|
$errorHandler = new \App\Services\ErrorHandler();
|
||||||
|
$errorHandler->handleException($e);
|
||||||
|
|
||||||
|
$_SESSION['error'] = 'Failed to delete notification group. Please try again.';
|
||||||
|
$this->redirect('/groups');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addChannel()
|
public function addChannel()
|
||||||
@@ -167,6 +194,13 @@ class NotificationGroupController extends Controller
|
|||||||
$groupId = (int)$_POST['group_id'];
|
$groupId = (int)$_POST['group_id'];
|
||||||
$channelType = $_POST['channel_type'] ?? '';
|
$channelType = $_POST['channel_type'] ?? '';
|
||||||
|
|
||||||
|
// Validate channel type
|
||||||
|
if (empty($channelType)) {
|
||||||
|
$_SESSION['error'] = 'Please select a channel type';
|
||||||
|
$this->redirect("/groups/edit?id=$groupId");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$config = $this->buildChannelConfig($channelType, $_POST);
|
$config = $this->buildChannelConfig($channelType, $_POST);
|
||||||
|
|
||||||
if (!$config) {
|
if (!$config) {
|
||||||
@@ -189,10 +223,18 @@ class NotificationGroupController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$this->channelModel->createChannel($groupId, $channelType, $config);
|
$this->channelModel->createChannel($groupId, $channelType, $config);
|
||||||
|
|
||||||
$_SESSION['success'] = 'Channel added successfully';
|
$_SESSION['success'] = 'Channel added successfully';
|
||||||
$this->redirect("/groups/edit?id=$groupId");
|
$this->redirect("/groups/edit?id=$groupId");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log the error using the ErrorHandler service
|
||||||
|
$errorHandler = new \App\Services\ErrorHandler();
|
||||||
|
$errorHandler->handleException($e);
|
||||||
|
|
||||||
|
$_SESSION['error'] = 'Failed to add notification channel. Please try again.';
|
||||||
|
$this->redirect("/groups/edit?id=$groupId");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteChannel()
|
public function deleteChannel()
|
||||||
@@ -200,10 +242,18 @@ class NotificationGroupController extends Controller
|
|||||||
$id = $_GET['id'] ?? 0;
|
$id = $_GET['id'] ?? 0;
|
||||||
$groupId = $_GET['group_id'] ?? 0;
|
$groupId = $_GET['group_id'] ?? 0;
|
||||||
|
|
||||||
|
try {
|
||||||
$this->channelModel->delete($id);
|
$this->channelModel->delete($id);
|
||||||
|
|
||||||
$_SESSION['success'] = 'Channel deleted successfully';
|
$_SESSION['success'] = 'Channel deleted successfully';
|
||||||
$this->redirect("/groups/edit?id=$groupId");
|
$this->redirect("/groups/edit?id=$groupId");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log the error using the ErrorHandler service
|
||||||
|
$errorHandler = new \App\Services\ErrorHandler();
|
||||||
|
$errorHandler->handleException($e);
|
||||||
|
|
||||||
|
$_SESSION['error'] = 'Failed to delete notification channel. Please try again.';
|
||||||
|
$this->redirect("/groups/edit?id=$groupId");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toggleChannel()
|
public function toggleChannel()
|
||||||
@@ -211,34 +261,170 @@ class NotificationGroupController extends Controller
|
|||||||
$id = $_GET['id'] ?? 0;
|
$id = $_GET['id'] ?? 0;
|
||||||
$groupId = $_GET['group_id'] ?? 0;
|
$groupId = $_GET['group_id'] ?? 0;
|
||||||
|
|
||||||
|
try {
|
||||||
$this->channelModel->toggleActive($id);
|
$this->channelModel->toggleActive($id);
|
||||||
|
|
||||||
$_SESSION['success'] = 'Channel status updated';
|
$_SESSION['success'] = 'Channel status updated';
|
||||||
$this->redirect("/groups/edit?id=$groupId");
|
$this->redirect("/groups/edit?id=$groupId");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log the error using the ErrorHandler service
|
||||||
|
$errorHandler = new \App\Services\ErrorHandler();
|
||||||
|
$errorHandler->handleException($e);
|
||||||
|
|
||||||
|
$_SESSION['error'] = 'Failed to update channel status. Please try again.';
|
||||||
|
$this->redirect("/groups/edit?id=$groupId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testChannel()
|
||||||
|
{
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
$this->redirect('/groups');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSRF Protection
|
||||||
|
$this->verifyCsrf('/groups');
|
||||||
|
|
||||||
|
$channelType = $_POST['channel_type'] ?? '';
|
||||||
|
$config = $this->buildChannelConfig($channelType, $_POST);
|
||||||
|
|
||||||
|
// Check if this is an AJAX request
|
||||||
|
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||||
|
|
||||||
|
if (!$config) {
|
||||||
|
if ($isAjax) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Invalid channel configuration for testing']);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
$_SESSION['error'] = 'Invalid channel configuration for testing';
|
||||||
|
$groupId = $_POST['group_id'] ?? 0;
|
||||||
|
$this->redirect($groupId ? "/groups/edit?id=$groupId" : '/groups');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$notificationService = new \App\Services\NotificationService();
|
||||||
|
$testMessage = $this->getTestMessage($channelType);
|
||||||
|
$testData = $this->getTestData();
|
||||||
|
|
||||||
|
$success = $notificationService->send($channelType, $config, $testMessage, $testData);
|
||||||
|
|
||||||
|
if ($success) {
|
||||||
|
$message = "Test message sent successfully to {$channelType} channel! Check your {$channelType} for the test notification.";
|
||||||
|
if ($isAjax) {
|
||||||
|
echo json_encode(['success' => true, 'message' => $message]);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
$_SESSION['success'] = $message;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$message = "Failed to send test message to {$channelType} channel. Please check your configuration and try again.";
|
||||||
|
if ($isAjax) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'message' => $message]);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
$_SESSION['error'] = $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Log the error using the ErrorHandler service
|
||||||
|
$errorHandler = new \App\Services\ErrorHandler();
|
||||||
|
$errorHandler->handleException($e);
|
||||||
|
|
||||||
|
$message = "Test failed: " . $e->getMessage() . " Please check your configuration and try again.";
|
||||||
|
if ($isAjax) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => $message]);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
$_SESSION['error'] = $message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only redirect if not AJAX
|
||||||
|
if (!$isAjax) {
|
||||||
|
$groupId = $_POST['group_id'] ?? 0;
|
||||||
|
$this->redirect("/groups/edit?id=$groupId");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTestMessage(string $channelType): string
|
||||||
|
{
|
||||||
|
$channelNames = [
|
||||||
|
'email' => 'Email',
|
||||||
|
'telegram' => 'Telegram',
|
||||||
|
'discord' => 'Discord',
|
||||||
|
'slack' => 'Slack'
|
||||||
|
];
|
||||||
|
|
||||||
|
$channelName = $channelNames[$channelType] ?? ucfirst($channelType);
|
||||||
|
|
||||||
|
return "🧪 **Test Message from Domain Monitor**\n\n" .
|
||||||
|
"This is a test notification to verify your {$channelName} channel configuration.\n\n" .
|
||||||
|
"✅ If you're seeing this message, your {$channelName} integration is working correctly!\n\n" .
|
||||||
|
"Test sent at: " . date('Y-m-d H:i:s T');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTestData(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'domain' => 'example.com',
|
||||||
|
'days_left' => 30,
|
||||||
|
'expiration_date' => date('Y-m-d', strtotime('+30 days')),
|
||||||
|
'registrar' => 'Example Registrar',
|
||||||
|
'domain_id' => 1
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildChannelConfig(string $type, array $data): ?array
|
private function buildChannelConfig(string $type, array $data): ?array
|
||||||
{
|
{
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'email':
|
case 'email':
|
||||||
if (empty($data['email'])) return null;
|
$email = trim($data['email'] ?? '');
|
||||||
return ['email' => $data['email']];
|
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ['email' => $email];
|
||||||
|
|
||||||
case 'telegram':
|
case 'telegram':
|
||||||
if (empty($data['bot_token']) || empty($data['chat_id'])) return null;
|
$botToken = trim($data['bot_token'] ?? '');
|
||||||
|
$chatId = trim($data['chat_id'] ?? '');
|
||||||
|
if (empty($botToken) || empty($chatId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Basic validation for bot token format
|
||||||
|
if (!preg_match('/^\d+:[A-Za-z0-9_-]+$/', $botToken)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
'bot_token' => $data['bot_token'],
|
'bot_token' => $botToken,
|
||||||
'chat_id' => $data['chat_id']
|
'chat_id' => $chatId
|
||||||
];
|
];
|
||||||
|
|
||||||
case 'discord':
|
case 'discord':
|
||||||
$webhookUrl = $data['discord_webhook_url'] ?? '';
|
$webhookUrl = trim($data['discord_webhook_url'] ?? '');
|
||||||
if (empty($webhookUrl)) return null;
|
if (empty($webhookUrl) || !filter_var($webhookUrl, FILTER_VALIDATE_URL)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Validate Discord webhook URL format
|
||||||
|
if (!str_contains($webhookUrl, 'discord.com/api/webhooks/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return ['webhook_url' => $webhookUrl];
|
return ['webhook_url' => $webhookUrl];
|
||||||
|
|
||||||
case 'slack':
|
case 'slack':
|
||||||
$webhookUrl = $data['slack_webhook_url'] ?? '';
|
$webhookUrl = trim($data['slack_webhook_url'] ?? '');
|
||||||
if (empty($webhookUrl)) return null;
|
if (empty($webhookUrl) || !filter_var($webhookUrl, FILTER_VALIDATE_URL)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Validate Slack webhook URL format
|
||||||
|
if (!str_contains($webhookUrl, 'hooks.slack.com/services/')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return ['webhook_url' => $webhookUrl];
|
return ['webhook_url' => $webhookUrl];
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -268,17 +454,30 @@ class NotificationGroupController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$deletedCount = 0;
|
$deletedCount = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
foreach ($groupIds as $groupId) {
|
foreach ($groupIds as $groupId) {
|
||||||
try {
|
try {
|
||||||
$this->groupModel->deleteWithRelations((int)$groupId);
|
$this->groupModel->deleteWithRelations((int)$groupId);
|
||||||
$deletedCount++;
|
$deletedCount++;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Continue with next group
|
// Log individual errors but continue processing
|
||||||
|
$errorHandler = new \App\Services\ErrorHandler();
|
||||||
|
$errorHandler->handleException($e);
|
||||||
|
$errors[] = "Failed to delete group ID: $groupId";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($deletedCount > 0) {
|
||||||
|
if (empty($errors)) {
|
||||||
$_SESSION['success'] = "Successfully deleted $deletedCount notification group(s)";
|
$_SESSION['success'] = "Successfully deleted $deletedCount notification group(s)";
|
||||||
|
} else {
|
||||||
|
$_SESSION['warning'] = "Deleted $deletedCount group(s), but " . count($errors) . " failed. Check error logs for details.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$_SESSION['error'] = 'Failed to delete any groups. Please check error logs for details.';
|
||||||
|
}
|
||||||
|
|
||||||
$this->redirect('/groups');
|
$this->redirect('/groups');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ ob_start();
|
|||||||
?>
|
?>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
|
<button onclick="testChannel('<?= $channel['channel_type'] ?>', <?= htmlspecialchars(json_encode($config)) ?>)"
|
||||||
|
class="flex-1 px-3 py-2 bg-blue-50 text-blue-700 rounded text-center text-sm hover:bg-blue-100 transition-colors duration-150">
|
||||||
|
<i class="fas fa-paper-plane mr-1"></i>
|
||||||
|
Test
|
||||||
|
</button>
|
||||||
<a href="/channels/toggle?id=<?= $channel['id'] ?>&group_id=<?= $group['id'] ?>"
|
<a href="/channels/toggle?id=<?= $channel['id'] ?>&group_id=<?= $group['id'] ?>"
|
||||||
class="flex-1 px-3 py-2 bg-yellow-50 text-yellow-700 rounded text-center text-sm hover:bg-yellow-100 transition-colors duration-150">
|
class="flex-1 px-3 py-2 bg-yellow-50 text-yellow-700 rounded text-center text-sm hover:bg-yellow-100 transition-colors duration-150">
|
||||||
<i class="fas fa-<?= $channel['is_active'] ? 'pause' : 'play' ?> mr-1"></i>
|
<i class="fas fa-<?= $channel['is_active'] ? 'pause' : 'play' ?> mr-1"></i>
|
||||||
@@ -231,11 +236,21 @@ ob_start();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="inline-flex items-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
|
class="inline-flex items-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
|
||||||
<i class="fas fa-plus mr-2"></i>
|
<i class="fas fa-plus mr-2"></i>
|
||||||
Add Channel
|
Add Channel
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
id="testChannelBtn"
|
||||||
|
onclick="testChannel()"
|
||||||
|
class="inline-flex items-center px-4 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors text-sm hidden">
|
||||||
|
<i class="fas fa-paper-plane mr-2"></i>
|
||||||
|
Test Channel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,6 +306,7 @@ ob_start();
|
|||||||
<script>
|
<script>
|
||||||
function toggleChannelFields() {
|
function toggleChannelFields() {
|
||||||
const channelType = document.getElementById('channel_type').value;
|
const channelType = document.getElementById('channel_type').value;
|
||||||
|
const testBtn = document.getElementById('testChannelBtn');
|
||||||
|
|
||||||
// Get all input fields
|
// Get all input fields
|
||||||
const emailField = document.getElementById('email');
|
const emailField = document.getElementById('email');
|
||||||
@@ -312,6 +328,9 @@ function toggleChannelFields() {
|
|||||||
document.getElementById('discord_fields').classList.add('hidden');
|
document.getElementById('discord_fields').classList.add('hidden');
|
||||||
document.getElementById('slack_fields').classList.add('hidden');
|
document.getElementById('slack_fields').classList.add('hidden');
|
||||||
|
|
||||||
|
// Hide test button by default
|
||||||
|
testBtn.classList.add('hidden');
|
||||||
|
|
||||||
// Show selected field and make required
|
// Show selected field and make required
|
||||||
if (channelType) {
|
if (channelType) {
|
||||||
document.getElementById(channelType + '_fields').classList.remove('hidden');
|
document.getElementById(channelType + '_fields').classList.remove('hidden');
|
||||||
@@ -334,6 +353,9 @@ function toggleChannelFields() {
|
|||||||
slackWebhook.focus();
|
slackWebhook.focus();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show test button when channel type is selected
|
||||||
|
testBtn.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,6 +404,240 @@ if (addChannelForm) {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test channel functionality - handles both new and existing channels
|
||||||
|
function testChannel(channelType, existingConfig = null) {
|
||||||
|
// If existingConfig is provided, we're testing an existing channel
|
||||||
|
// If not, we're testing a new channel from the form
|
||||||
|
const isExistingChannel = existingConfig !== null;
|
||||||
|
|
||||||
|
if (!isExistingChannel) {
|
||||||
|
// For new channels, get values from form
|
||||||
|
channelType = document.getElementById('channel_type').value;
|
||||||
|
const testBtn = document.getElementById('testChannelBtn');
|
||||||
|
|
||||||
|
if (!channelType) {
|
||||||
|
alert('Please select a channel type first');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields before testing
|
||||||
|
let isValid = true;
|
||||||
|
let errorMessage = '';
|
||||||
|
|
||||||
|
switch(channelType) {
|
||||||
|
case 'email':
|
||||||
|
const email = document.getElementById('email').value.trim();
|
||||||
|
if (!email) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'Please enter an email address';
|
||||||
|
} else if (!email.includes('@') || !email.includes('.')) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'Please enter a valid email address';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'telegram':
|
||||||
|
const botToken = document.getElementById('bot_token').value.trim();
|
||||||
|
const chatId = document.getElementById('chat_id').value.trim();
|
||||||
|
if (!botToken) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'Please enter a bot token';
|
||||||
|
} else if (!/^\d+:[A-Za-z0-9_-]+$/.test(botToken)) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'Please enter a valid bot token format (e.g., 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11)';
|
||||||
|
} else if (!chatId) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'Please enter a chat ID';
|
||||||
|
} else if (!/^-?\d+$/.test(chatId)) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'Please enter a valid chat ID (numeric value)';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discord':
|
||||||
|
const discordWebhook = document.getElementById('discord_webhook').value.trim();
|
||||||
|
if (!discordWebhook) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'Please enter a Discord webhook URL';
|
||||||
|
} else if (!discordWebhook.includes('discord.com/api/webhooks/')) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'Invalid Discord webhook URL';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'slack':
|
||||||
|
const slackWebhook = document.getElementById('slack_webhook').value.trim();
|
||||||
|
if (!slackWebhook) {
|
||||||
|
isValid = false;
|
||||||
|
errorMessage = 'Please enter a Slack webhook URL';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
alert(errorMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable button and show loading state for new channels
|
||||||
|
testBtn.disabled = true;
|
||||||
|
testBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Testing...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create form data for AJAX request
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('channel_type', channelType);
|
||||||
|
|
||||||
|
// Add group ID
|
||||||
|
const groupId = document.querySelector('input[name="group_id"]').value;
|
||||||
|
formData.append('group_id', groupId);
|
||||||
|
|
||||||
|
// Add CSRF token
|
||||||
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
|
formData.append('csrf_token', csrfToken);
|
||||||
|
|
||||||
|
// Add channel-specific data
|
||||||
|
if (isExistingChannel) {
|
||||||
|
// Use existing channel config
|
||||||
|
switch(channelType) {
|
||||||
|
case 'email':
|
||||||
|
formData.append('email', existingConfig.email);
|
||||||
|
break;
|
||||||
|
case 'telegram':
|
||||||
|
formData.append('bot_token', existingConfig.bot_token);
|
||||||
|
formData.append('chat_id', existingConfig.chat_id);
|
||||||
|
break;
|
||||||
|
case 'discord':
|
||||||
|
formData.append('discord_webhook_url', existingConfig.webhook_url);
|
||||||
|
break;
|
||||||
|
case 'slack':
|
||||||
|
formData.append('slack_webhook_url', existingConfig.webhook_url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use form values for new channels
|
||||||
|
switch(channelType) {
|
||||||
|
case 'email':
|
||||||
|
formData.append('email', document.getElementById('email').value);
|
||||||
|
break;
|
||||||
|
case 'telegram':
|
||||||
|
formData.append('bot_token', document.getElementById('bot_token').value);
|
||||||
|
formData.append('chat_id', document.getElementById('chat_id').value);
|
||||||
|
break;
|
||||||
|
case 'discord':
|
||||||
|
formData.append('discord_webhook_url', document.getElementById('discord_webhook').value);
|
||||||
|
break;
|
||||||
|
case 'slack':
|
||||||
|
formData.append('slack_webhook_url', document.getElementById('slack_webhook').value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send AJAX request
|
||||||
|
fetch('/channels/test', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Reset button for new channels
|
||||||
|
if (!isExistingChannel) {
|
||||||
|
const testBtn = document.getElementById('testChannelBtn');
|
||||||
|
testBtn.disabled = false;
|
||||||
|
testBtn.innerHTML = '<i class="fas fa-paper-plane mr-2"></i>Test Channel';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showToast(data.message, 'success');
|
||||||
|
} else {
|
||||||
|
showToast(data.message, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Reset button for new channels
|
||||||
|
if (!isExistingChannel) {
|
||||||
|
const testBtn = document.getElementById('testChannelBtn');
|
||||||
|
testBtn.disabled = false;
|
||||||
|
testBtn.innerHTML = '<i class="fas fa-paper-plane mr-2"></i>Test Channel';
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('❌ Test failed: ' + error.message + ' Please check your configuration and try again.', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Function to show toast messages dynamically
|
||||||
|
function showToast(message, type = 'info') {
|
||||||
|
const toastContainer = document.getElementById('toast-container');
|
||||||
|
if (!toastContainer) return;
|
||||||
|
|
||||||
|
const typeConfig = {
|
||||||
|
success: {
|
||||||
|
icon: 'fa-check',
|
||||||
|
iconColor: 'text-green-600',
|
||||||
|
bgColor: 'bg-green-100',
|
||||||
|
borderColor: 'border-green-500',
|
||||||
|
title: 'Success'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
icon: 'fa-times',
|
||||||
|
iconColor: 'text-red-600',
|
||||||
|
bgColor: 'bg-red-100',
|
||||||
|
borderColor: 'border-red-500',
|
||||||
|
title: 'Error'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
icon: 'fa-exclamation-triangle',
|
||||||
|
iconColor: 'text-orange-600',
|
||||||
|
bgColor: 'bg-orange-100',
|
||||||
|
borderColor: 'border-orange-500',
|
||||||
|
title: 'Warning'
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
icon: 'fa-info',
|
||||||
|
iconColor: 'text-blue-600',
|
||||||
|
bgColor: 'bg-blue-100',
|
||||||
|
borderColor: 'border-blue-500',
|
||||||
|
title: 'Info'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = typeConfig[type] || typeConfig.info;
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast bg-white border-l-4 ${config.borderColor} rounded-lg shadow-lg p-4 flex items-start animate-slide-in`;
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-8 h-8 ${config.bgColor} rounded-full flex items-center justify-center">
|
||||||
|
<i class="fas ${config.icon} ${config.iconColor} text-sm"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex-1">
|
||||||
|
<p class="text-sm font-medium text-gray-900">${config.title}</p>
|
||||||
|
<p class="text-sm text-gray-600 mt-0.5">${message}</p>
|
||||||
|
</div>
|
||||||
|
<button onclick="this.parentElement.remove()" class="ml-3 flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors">
|
||||||
|
<i class="fas fa-times text-sm"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
|
||||||
|
// Auto-dismiss after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.transition = 'opacity 0.3s ease-out, transform 0.3s ease-out';
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
toast.style.transform = 'translateX(100%)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- Toast Notifications Container -->
|
<!-- Toast Notifications Container -->
|
||||||
<div id="toast-container" class="fixed bottom-4 right-4 z-50 space-y-3 max-w-sm">
|
<div id="toast-container" class="fixed bottom-4 right-4 z-[9999] space-y-3 max-w-sm">
|
||||||
|
|
||||||
<!-- Success Toast -->
|
<!-- Success Toast -->
|
||||||
<?php if (isset($_SESSION['success'])): ?>
|
<?php if (isset($_SESSION['success'])): ?>
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ $router->post('/groups/bulk-delete', [NotificationGroupController::class, 'bulkD
|
|||||||
$router->post('/channels/add', [NotificationGroupController::class, 'addChannel']);
|
$router->post('/channels/add', [NotificationGroupController::class, 'addChannel']);
|
||||||
$router->get('/channels/delete', [NotificationGroupController::class, 'deleteChannel']);
|
$router->get('/channels/delete', [NotificationGroupController::class, 'deleteChannel']);
|
||||||
$router->get('/channels/toggle', [NotificationGroupController::class, 'toggleChannel']);
|
$router->get('/channels/toggle', [NotificationGroupController::class, 'toggleChannel']);
|
||||||
|
$router->post('/channels/test', [NotificationGroupController::class, 'testChannel']);
|
||||||
|
|
||||||
// TLD Registry
|
// TLD Registry
|
||||||
$router->get('/tld-registry', [TldRegistryController::class, 'index']);
|
$router->get('/tld-registry', [TldRegistryController::class, 'index']);
|
||||||
|
|||||||
Reference in New Issue
Block a user