- Express.js backend with JWT authentication - CRUD endpoints for apps management - Health check endpoint - Dockerfile per admin API (Node 18 Alpine) - Kubernetes: admin-api deployment, service, ingress - Admin panel at http://admin.apps.local - Updated nginx.conf to route /api to admin API - Fixed ingress rules for separate web and admin services
245 lines
9.0 KiB
JavaScript
245 lines
9.0 KiB
JavaScript
const API_URL = 'http://localhost:3000';
|
|
let authToken = localStorage.getItem('admin_token');
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
checkAuth();
|
|
if (authToken) {
|
|
loadApps();
|
|
}
|
|
});
|
|
|
|
function checkAuth() {
|
|
const loginSection = document.getElementById('login-section');
|
|
const adminSection = document.getElementById('admin-section');
|
|
const userStatus = document.getElementById('user-status');
|
|
|
|
if (authToken) {
|
|
loginSection.classList.add('hidden');
|
|
adminSection.classList.remove('hidden');
|
|
userStatus.classList.remove('hidden');
|
|
userStatus.innerHTML = '<i class="fas fa-user-shield mr-2"></i>Admin';
|
|
}
|
|
}
|
|
|
|
async function login() {
|
|
const tokenInput = document.getElementById('admin-token');
|
|
const token = tokenInput.value;
|
|
const errorDiv = document.getElementById('error-message');
|
|
|
|
if (!token) {
|
|
showError('Inserisci il token admin');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/api/admin/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
authToken = data.apiKey;
|
|
localStorage.setItem('admin_token', authToken);
|
|
checkAuth();
|
|
loadApps();
|
|
tokenInput.value = '';
|
|
} else {
|
|
showError(data.error || 'Login fallito');
|
|
}
|
|
} catch (error) {
|
|
showError('Errore connessione API: ' + error.message);
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('admin_token');
|
|
authToken = null;
|
|
checkAuth();
|
|
}
|
|
|
|
async function loadApps() {
|
|
const container = document.getElementById('apps-list');
|
|
container.innerHTML = '<div class="col-span-full text-center py-10"><i class="fas fa-spinner fa-spin text-3xl"></i></div>';
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/api/apps`);
|
|
const data = await response.json();
|
|
renderApps(data.apps || data);
|
|
} catch (error) {
|
|
container.innerHTML = '<div class="col-span-full text-center text-red-400 py-10">Errore caricamento app</div>';
|
|
}
|
|
}
|
|
|
|
function renderApps(apps) {
|
|
const container = document.getElementById('apps-list');
|
|
|
|
if (!apps || apps.length === 0) {
|
|
container.innerHTML = '<div class="col-span-full text-center py-10 text-gray-500">Nessuna applicazione. Aggiungine una!</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = '';
|
|
|
|
apps.forEach(app => {
|
|
const appCard = document.createElement('div');
|
|
appCard.className = 'bg-gray-850 rounded-xl overflow-hidden shadow-lg border border-gray-700 hover:shadow-xl transition-shadow duration-300';
|
|
appCard.innerHTML = `
|
|
<div class="p-6">
|
|
<div class="flex items-start justify-between mb-4">
|
|
<div class="flex items-center">
|
|
<div class="w-12 h-12 rounded-lg bg-gradient-to-br ${app.colors || 'from-gray-600 to-gray-800'} flex items-center justify-center mr-4">
|
|
<i class="${app.icon || 'fa-package'} fa-xl text-white"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-bold">${app.name}</h3>
|
|
<span class="text-xs text-gray-500 font-mono">ID: ${app.id}</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
<button onclick="editApp(${app.id})" class="px-3 py-1 text-xs bg-blue-600 hover:bg-blue-700 rounded">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button onclick="deleteApp(${app.id})" class="px-3 py-1 text-xs bg-red-600 hover:bg-red-700 rounded">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p class="text-gray-400 text-sm mb-4">${app.description || ''}</p>
|
|
<div class="bg-gray-900 rounded-lg p-2 space-y-1">
|
|
<div class="flex items-center text-xs text-gray-500">
|
|
<i class="fas fa-terminal mr-2"></i>APT: ${app.packages?.apt || 'N/A'}
|
|
</div>
|
|
<div class="flex items-center text-xs text-gray-500">
|
|
<i class="fas fa-code mr-2"></i>AppStream: ${app.packages?.appstream || 'N/A'}
|
|
</div>
|
|
<div class="flex items-center text-xs text-gray-500">
|
|
<i class="fas fa-cloud-download-alt mr-2"></i>Flatpak: ${app.packages?.flatpak || 'N/A'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.appendChild(appCard);
|
|
});
|
|
}
|
|
|
|
function showError(message) {
|
|
const errorDiv = document.getElementById('error-message');
|
|
errorDiv.textContent = message;
|
|
errorDiv.classList.remove('hidden');
|
|
setTimeout(() => errorDiv.classList.add('hidden'), 5000);
|
|
}
|
|
|
|
function showAddForm() {
|
|
document.getElementById('form-title').textContent = 'Aggiungi Applicazione';
|
|
document.getElementById('app-form').reset();
|
|
document.getElementById('app-id').value = '';
|
|
document.getElementById('app-id-input').value = '';
|
|
document.getElementById('app-name').value = '';
|
|
document.getElementById('app-description').value = '';
|
|
document.getElementById('app-icon').value = 'fa-package';
|
|
document.getElementById('app-colors').value = 'from-purple-500 to-indigo-600';
|
|
document.getElementById('app-apt').value = '';
|
|
document.getElementById('app-appstream').value = '';
|
|
document.getElementById('app-flatpak').value = '';
|
|
document.getElementById('app-form-modal').classList.remove('hidden');
|
|
}
|
|
|
|
function closeForm() {
|
|
document.getElementById('app-form-modal').classList.add('hidden');
|
|
}
|
|
|
|
async function saveApp(event) {
|
|
event.preventDefault();
|
|
|
|
const idInput = document.getElementById('app-id-input').value;
|
|
const name = document.getElementById('app-name').value;
|
|
const description = document.getElementById('app-description').value;
|
|
const icon = document.getElementById('app-icon').value;
|
|
const colors = document.getElementById('app-colors').value;
|
|
const apt = document.getElementById('app-apt').value;
|
|
const appstream = document.getElementById('app-appstream').value;
|
|
const flatpak = document.getElementById('app-flatpak').value;
|
|
|
|
const appData = {
|
|
id: idInput,
|
|
name,
|
|
description,
|
|
icon,
|
|
colors,
|
|
packages: { apt, appstream, flatpak }
|
|
};
|
|
|
|
const isEdit = document.getElementById('app-id').value;
|
|
const method = isEdit ? 'PUT' : 'POST';
|
|
const url = isEdit ? `${API_URL}/api/admin/apps/${document.getElementById('app-id').value}` : `${API_URL}/api/admin/apps`;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${authToken}`
|
|
},
|
|
body: JSON.stringify(appData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
closeForm();
|
|
loadApps();
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.error || 'Salvataggio fallito');
|
|
}
|
|
} catch (error) {
|
|
showError('Errore: ' + error.message);
|
|
}
|
|
}
|
|
|
|
async function editApp(id) {
|
|
const apps = await fetch(`${API_URL}/api/apps`).then(r => r.json());
|
|
const app = (apps.apps || apps).find(a => a.id === id);
|
|
|
|
if (!app) return;
|
|
|
|
document.getElementById('form-title').textContent = 'Modifica Applicazione';
|
|
document.getElementById('app-id').value = app.id;
|
|
document.getElementById('app-id-input').value = app.id;
|
|
document.getElementById('app-name').value = app.name;
|
|
document.getElementById('app-description').value = app.description;
|
|
document.getElementById('app-icon').value = app.icon || 'fa-package';
|
|
document.getElementById('app-colors').value = app.colors || 'from-purple-500 to-indigo-600';
|
|
document.getElementById('app-apt').value = app.packages?.apt || '';
|
|
document.getElementById('app-appstream').value = app.packages?.appstream || '';
|
|
document.getElementById('app-flatpak').value = app.packages?.flatpak || '';
|
|
document.getElementById('app-form-modal').classList.remove('hidden');
|
|
}
|
|
|
|
async function deleteApp(id) {
|
|
if (!confirm('Vuoi eliminare questa applicazione?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/api/admin/apps/${id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
loadApps();
|
|
} else {
|
|
showError('Eliminazione fallita');
|
|
}
|
|
} catch (error) {
|
|
showError('Errore: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Close modal on outside click
|
|
document.getElementById('app-form-modal').addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('fixed')) {
|
|
closeForm();
|
|
}
|
|
});
|