proxy-oidcv2/public/admin.html
2025-12-03 21:34:44 +01:00

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>