814 lines
22 KiB
HTML
814 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin Panel - Secure Proxy</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--primary: #667eea;
|
|
--primary-dark: #764ba2;
|
|
--success: #48bb78;
|
|
--danger: #f56565;
|
|
--warning: #ed8936;
|
|
--info: #4299e1;
|
|
--light: #f7fafc;
|
|
--dark: #2d3748;
|
|
--border: #e2e8f0;
|
|
--shadow: 0 10px 25px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
background: #f5f7fa;
|
|
color: #2d3748;
|
|
}
|
|
|
|
.navbar {
|
|
background: white;
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 1rem 2rem;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.navbar h1 {
|
|
font-size: 1.5rem;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.navbar-right {
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.user-info {
|
|
font-size: 0.9rem;
|
|
color: #718096;
|
|
}
|
|
|
|
button, a.btn {
|
|
padding: 0.5rem 1rem;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
transition: all 0.3s;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: var(--primary-dark);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--light);
|
|
color: var(--dark);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background: #edf2f7;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: var(--danger);
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #e53e3e;
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 0.35rem 0.75rem;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 2rem;
|
|
margin-bottom: 2rem;
|
|
color: var(--dark);
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
padding: 1.5rem;
|
|
border-radius: 8px;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.stat-label {
|
|
color: #718096;
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: var(--shadow);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.card-header {
|
|
padding: 1.5rem;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.card-body {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
label {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
font-weight: 500;
|
|
color: var(--dark);
|
|
}
|
|
|
|
input, textarea, select {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
font-family: inherit;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
input:focus, textarea:focus, select:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
}
|
|
|
|
textarea {
|
|
resize: vertical;
|
|
min-height: 100px;
|
|
}
|
|
|
|
.checkbox-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
input[type="checkbox"] {
|
|
width: auto;
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.table thead {
|
|
background: var(--light);
|
|
border-bottom: 2px solid var(--border);
|
|
}
|
|
|
|
.table th, .table td {
|
|
padding: 1rem;
|
|
text-align: left;
|
|
}
|
|
|
|
.table tbody tr {
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.table tbody tr:hover {
|
|
background: var(--light);
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.badge-success {
|
|
background: #c6f6d5;
|
|
color: #22543d;
|
|
}
|
|
|
|
.badge-danger {
|
|
background: #fed7d7;
|
|
color: #742a2a;
|
|
}
|
|
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
z-index: 1000;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(0,0,0,0.5);
|
|
}
|
|
|
|
.modal.show {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal-content {
|
|
background-color: white;
|
|
padding: 2rem;
|
|
border-radius: 8px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.modal-header {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.modal-header h2 {
|
|
color: var(--dark);
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
gap: 1rem;
|
|
justify-content: flex-end;
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.alert {
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
margin-bottom: 1rem;
|
|
display: none;
|
|
}
|
|
|
|
.alert.show {
|
|
display: block;
|
|
}
|
|
|
|
.alert-success {
|
|
background: #c6f6d5;
|
|
color: #22543d;
|
|
border: 1px solid #9ae6b4;
|
|
}
|
|
|
|
.alert-danger {
|
|
background: #fed7d7;
|
|
color: #742a2a;
|
|
border: 1px solid #fc8181;
|
|
}
|
|
|
|
.alert-info {
|
|
background: #bee3f8;
|
|
color: #2c5282;
|
|
border: 1px solid #90cdf4;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: #718096;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
border-bottom: 2px solid var(--border);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.tab {
|
|
padding: 1rem 1.5rem;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: #718096;
|
|
font-weight: 500;
|
|
border-bottom: 3px solid transparent;
|
|
margin-bottom: -2px;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.tab.active {
|
|
color: var(--primary);
|
|
border-bottom-color: var(--primary);
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.search-box {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.search-box input {
|
|
max-width: 300px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.navbar {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.form-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.table {
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.table th, .table td {
|
|
padding: 0.75rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="navbar">
|
|
<h1>🔐 Secure Proxy Admin</h1>
|
|
<div class="navbar-right">
|
|
<div class="user-info">
|
|
<span id="username">User</span>
|
|
</div>
|
|
<a href="/" class="btn btn-secondary btn-small">Home</a>
|
|
<a href="/auth/logout" class="btn btn-secondary btn-small">Logout</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="alert" id="alertBox"></div>
|
|
|
|
<!-- Stats Section -->
|
|
<div class="stats-grid" id="statsGrid">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Services</div>
|
|
<div class="stat-value" id="totalServices">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Enabled Services</div>
|
|
<div class="stat-value" id="enabledServices">0</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Disabled Services</div>
|
|
<div class="stat-value" id="disabledServices">0</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="tabs">
|
|
<button class="tab active" data-tab="services">Services</button>
|
|
<button class="tab" data-tab="logs">Audit Logs</button>
|
|
</div>
|
|
|
|
<!-- Services Tab -->
|
|
<div id="services" class="tab-content active">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
|
<h2 class="page-title" style="margin: 0;">Manage Services</h2>
|
|
<button class="btn btn-primary" id="newServiceBtn">+ New Service</button>
|
|
</div>
|
|
|
|
<div class="search-box">
|
|
<input type="text" id="searchServices" placeholder="Search services by name or path...">
|
|
</div>
|
|
|
|
<div id="servicesContainer" class="loading">Loading services...</div>
|
|
</div>
|
|
|
|
<!-- Logs Tab -->
|
|
<div id="logs" class="tab-content">
|
|
<h2 class="page-title" style="margin-top: 0;">Audit Logs</h2>
|
|
<div id="logsContainer" class="loading">Loading logs...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal for creating/editing services -->
|
|
<div class="modal" id="serviceModal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 id="modalTitle">Create Service</h2>
|
|
</div>
|
|
<form id="serviceForm">
|
|
<div class="form-group">
|
|
<label for="serviceName">Service Name *</label>
|
|
<input type="text" id="serviceName" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="servicePath">Path (e.g., /myapp) *</label>
|
|
<input type="text" id="servicePath" placeholder="/myapp" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="serviceUrl">Target URL *</label>
|
|
<input type="url" id="serviceUrl" placeholder="http://localhost:8080" required>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="serviceDescription">Description</label>
|
|
<textarea id="serviceDescription" placeholder="Optional description..."></textarea>
|
|
</div>
|
|
|
|
<div class="form-group checkbox-group">
|
|
<input type="checkbox" id="serviceAuth" checked>
|
|
<label for="serviceAuth" style="margin: 0;">Require Authentication</label>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" id="cancelBtn">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" id="saveBtn">Create Service</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = '/api';
|
|
const DASHBOARD_BASE = '/dashboard';
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadUserProfile();
|
|
loadDashboardStats();
|
|
loadServices();
|
|
attachEventListeners();
|
|
});
|
|
|
|
// Load user profile
|
|
async function loadUserProfile() {
|
|
try {
|
|
const response = await fetch('/auth/profile');
|
|
const data = await response.json();
|
|
document.getElementById('username').textContent = data.user.name || data.user.email;
|
|
} catch (error) {
|
|
console.error('Failed to load user profile:', error);
|
|
}
|
|
}
|
|
|
|
// Load dashboard stats
|
|
async function loadDashboardStats() {
|
|
try {
|
|
const response = await fetch(`${DASHBOARD_BASE}/stats`);
|
|
const stats = await response.json();
|
|
|
|
document.getElementById('totalServices').textContent = stats.totalServices;
|
|
document.getElementById('enabledServices').textContent = stats.enabledServices;
|
|
document.getElementById('disabledServices').textContent = stats.disabledServices;
|
|
} catch (error) {
|
|
console.error('Failed to load stats:', error);
|
|
}
|
|
}
|
|
|
|
// Load services
|
|
async function loadServices() {
|
|
const container = document.getElementById('servicesContainer');
|
|
try {
|
|
const response = await fetch(`${API_BASE}/services`);
|
|
const services = await response.json();
|
|
|
|
if (services.length === 0) {
|
|
container.innerHTML = '<p style="text-align: center; color: #718096; padding: 2rem;">No services yet. Create one to get started.</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '<table class="table"><thead><tr><th>Name</th><th>Path</th><th>Target URL</th><th>Auth</th><th>Status</th><th>Actions</th></tr></thead><tbody>';
|
|
|
|
services.forEach(service => {
|
|
const authBadge = service.requireAuth ? '<span class="badge badge-success">Required</span>' : '<span class="badge badge-danger">Disabled</span>';
|
|
const statusBadge = service.enabled ? '<span class="badge badge-success">Enabled</span>' : '<span class="badge badge-danger">Disabled</span>';
|
|
|
|
html += `
|
|
<tr>
|
|
<td>${escapeHtml(service.name)}</td>
|
|
<td><code>${escapeHtml(service.path)}</code></td>
|
|
<td>${escapeHtml(service.targetUrl)}</td>
|
|
<td>${authBadge}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-secondary btn-small" onclick="editService('${service.id}')">Edit</button>
|
|
<button class="btn btn-secondary btn-small" onclick="toggleService('${service.id}', ${!service.enabled})">
|
|
${service.enabled ? 'Disable' : 'Enable'}
|
|
</button>
|
|
<button class="btn btn-danger btn-small" onclick="deleteService('${service.id}')">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
html += '</tbody></table>';
|
|
container.innerHTML = html;
|
|
} catch (error) {
|
|
console.error('Failed to load services:', error);
|
|
container.innerHTML = '<p style="color: var(--danger);">Failed to load services</p>';
|
|
}
|
|
}
|
|
|
|
// Load audit logs
|
|
async function loadLogs() {
|
|
const container = document.getElementById('logsContainer');
|
|
try {
|
|
const response = await fetch(`${DASHBOARD_BASE}/logs?limit=50`);
|
|
const data = await response.json();
|
|
|
|
if (data.logs.length === 0) {
|
|
container.innerHTML = '<p style="text-align: center; color: #718096; padding: 2rem;">No audit logs yet.</p>';
|
|
return;
|
|
}
|
|
|
|
let html = '<table class="table"><thead><tr><th>Action</th><th>User</th><th>Service</th><th>Timestamp</th></tr></thead><tbody>';
|
|
|
|
data.logs.forEach(log => {
|
|
const timestamp = new Date(log.timestamp).toLocaleString();
|
|
html += `
|
|
<tr>
|
|
<td><code>${escapeHtml(log.action)}</code></td>
|
|
<td>${escapeHtml(log.user_id || 'N/A')}</td>
|
|
<td>${escapeHtml(log.service_id || 'N/A')}</td>
|
|
<td>${timestamp}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
html += '</tbody></table>';
|
|
container.innerHTML = html;
|
|
} catch (error) {
|
|
console.error('Failed to load logs:', error);
|
|
container.innerHTML = '<p style="color: var(--danger);">Failed to load logs</p>';
|
|
}
|
|
}
|
|
|
|
// Attach event listeners
|
|
function attachEventListeners() {
|
|
// Tab switching
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', (e) => {
|
|
const tabName = e.target.getAttribute('data-tab');
|
|
switchTab(tabName);
|
|
});
|
|
});
|
|
|
|
// New service button
|
|
document.getElementById('newServiceBtn').addEventListener('click', openNewServiceModal);
|
|
|
|
// Service form
|
|
document.getElementById('serviceForm').addEventListener('submit', handleServiceSubmit);
|
|
|
|
// Cancel button
|
|
document.getElementById('cancelBtn').addEventListener('click', closeModal);
|
|
|
|
// Search services
|
|
document.getElementById('searchServices').addEventListener('input', (e) => {
|
|
filterServices(e.target.value);
|
|
});
|
|
|
|
// Close modal on outside click
|
|
document.getElementById('serviceModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'serviceModal') {
|
|
closeModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Switch tabs
|
|
function switchTab(tabName) {
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
content.classList.remove('active');
|
|
});
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
tab.classList.remove('active');
|
|
});
|
|
|
|
document.getElementById(tabName).classList.add('active');
|
|
document.querySelector(`.tab[data-tab="${tabName}"]`).classList.add('active');
|
|
|
|
if (tabName === 'logs') {
|
|
loadLogs();
|
|
} else if (tabName === 'services') {
|
|
loadServices();
|
|
}
|
|
}
|
|
|
|
// Service modal
|
|
function openNewServiceModal() {
|
|
document.getElementById('serviceForm').reset();
|
|
document.getElementById('modalTitle').textContent = 'Create Service';
|
|
document.getElementById('saveBtn').textContent = 'Create Service';
|
|
document.getElementById('serviceModal').classList.add('show');
|
|
document.getElementById('serviceForm').dataset.mode = 'create';
|
|
}
|
|
|
|
async function editService(id) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/services/${id}`);
|
|
const service = await response.json();
|
|
|
|
document.getElementById('serviceName').value = service.name;
|
|
document.getElementById('servicePath').value = service.path;
|
|
document.getElementById('serviceUrl').value = service.targetUrl;
|
|
document.getElementById('serviceDescription').value = service.description || '';
|
|
document.getElementById('serviceAuth').checked = service.requireAuth;
|
|
|
|
document.getElementById('modalTitle').textContent = 'Edit Service';
|
|
document.getElementById('saveBtn').textContent = 'Update Service';
|
|
document.getElementById('serviceModal').classList.add('show');
|
|
document.getElementById('serviceForm').dataset.mode = 'edit';
|
|
document.getElementById('serviceForm').dataset.id = id;
|
|
} catch (error) {
|
|
showAlert('Failed to load service', 'danger');
|
|
}
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('serviceModal').classList.remove('show');
|
|
}
|
|
|
|
// Handle service form submission
|
|
async function handleServiceSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const formData = {
|
|
name: document.getElementById('serviceName').value,
|
|
path: document.getElementById('servicePath').value,
|
|
targetUrl: document.getElementById('serviceUrl').value,
|
|
description: document.getElementById('serviceDescription').value,
|
|
requireAuth: document.getElementById('serviceAuth').checked,
|
|
};
|
|
|
|
const mode = document.getElementById('serviceForm').dataset.mode;
|
|
const method = mode === 'create' ? 'POST' : 'PUT';
|
|
const url = mode === 'create'
|
|
? `${API_BASE}/services`
|
|
: `${API_BASE}/services/${document.getElementById('serviceForm').dataset.id}`;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
|
},
|
|
body: JSON.stringify(formData),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.error);
|
|
}
|
|
|
|
showAlert(
|
|
mode === 'create' ? 'Service created successfully' : 'Service updated successfully',
|
|
'success'
|
|
);
|
|
closeModal();
|
|
loadServices();
|
|
loadDashboardStats();
|
|
} catch (error) {
|
|
showAlert(`Error: ${error.message}`, 'danger');
|
|
}
|
|
}
|
|
|
|
// Toggle service
|
|
async function toggleService(id, enabled) {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/services/${id}/toggle`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
|
},
|
|
body: JSON.stringify({ enabled }),
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to toggle service');
|
|
|
|
showAlert('Service updated', 'success');
|
|
loadServices();
|
|
loadDashboardStats();
|
|
} catch (error) {
|
|
showAlert(`Error: ${error.message}`, 'danger');
|
|
}
|
|
}
|
|
|
|
// Delete service
|
|
async function deleteService(id) {
|
|
if (!confirm('Are you sure you want to delete this service?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/services/${id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
|
},
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to delete service');
|
|
|
|
showAlert('Service deleted successfully', 'success');
|
|
loadServices();
|
|
loadDashboardStats();
|
|
} catch (error) {
|
|
showAlert(`Error: ${error.message}`, 'danger');
|
|
}
|
|
}
|
|
|
|
// Filter services
|
|
function filterServices(query) {
|
|
const rows = document.querySelectorAll('table tbody tr');
|
|
rows.forEach(row => {
|
|
const text = row.textContent.toLowerCase();
|
|
row.style.display = text.includes(query.toLowerCase()) ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
// Show alert
|
|
function showAlert(message, type = 'info') {
|
|
const alertBox = document.getElementById('alertBox');
|
|
alertBox.textContent = message;
|
|
alertBox.className = `alert alert-${type} show`;
|
|
setTimeout(() => {
|
|
alertBox.classList.remove('show');
|
|
}, 4000);
|
|
}
|
|
|
|
// Utility function to escape HTML
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|