Add admin panel API with CRUD for apps

- 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
This commit is contained in:
capitano
2026-03-18 23:01:42 +01:00
parent 533cbed8d3
commit e89989e1d9
17 changed files with 857 additions and 7 deletions

111
frontend/admin.html Normal file
View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin - Linux App Store</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: { extend: { colors: { gray: { 850: '#1f2937', 900: '#111827' } } } }
}
</script>
</head>
<body class="bg-gray-900 text-white">
<nav class="bg-gray-850 border-b border-gray-700">
<div class="max-w-7xl mx-auto px-4">
<div class="flex items-center h-16">
<i class="fas fa-cog text-purple-500 text-xl mr-3"></i>
<span class="text-xl font-bold">Admin Panel</span>
<span id="user-status" class="ml-auto hidden text-sm text-gray-400"></span>
<button onclick="logout()" class="ml-4 px-3 py-1 text-sm bg-red-600 hover:bg-red-700 rounded">Logout</button>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto px-4 py-8">
<div id="login-section" class="max-w-md mx-auto mt-20">
<div class="bg-gray-850 rounded-xl p-8 shadow-2xl">
<h2 class="text-2xl font-bold mb-6 text-center">Accesso Amministratore</h2>
<div id="error-message" class="hidden bg-red-900 text-red-200 px-4 py-3 rounded mb-4"></div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Token Admin</label>
<input type="password" id="admin-token" class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 focus:ring-2 focus:ring-purple-500 focus:border-transparent">
</div>
<button onclick="login()" class="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded transition">
Accedi
</button>
</div>
</div>
</div>
<div id="admin-section" class="hidden">
<div class="flex justify-between items-center mb-6">
<h2 class="text-3xl font-bold">Gestione Applicazioni</h2>
<button onclick="showAddForm()" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg transition shadow-lg">
<i class="fas fa-plus mr-2"></i>Aggiungi App
</button>
</div>
<div id="apps-list" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
</div>
</div>
</main>
<!-- Add/Edit Form Modal -->
<div id="app-form-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div class="bg-gray-850 rounded-xl p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto shadow-2xl">
<h3 class="text-2xl font-bold mb-4" id="form-title">Aggiungi Applicazione</h3>
<form id="app-form" onsubmit="saveApp(event)">
<input type="hidden" id="app-id">
<div class="grid grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium mb-1">ID Unico</label>
<input type="text" id="app-id-input" required class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium mb-1">Nome</label>
<input type="text" id="app-name" required class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2">
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-1">Descrizione</label>
<textarea id="app-description" required class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 h-24"></textarea>
</div>
<div class="grid grid-cols-3 gap-4 mb-4">
<div>
<label class="block text-sm font-medium mb-1">Icona (Font Awesome)</label>
<input type="text" id="app-icon" placeholder="fa-volume-up" class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2">
</div>
<div>
<label class="block text-sm font-medium mb-1">Colori Gradient</label>
<input type="text" id="app-colors" placeholder="from-purple-500 to-indigo-600" class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2">
</div>
</div>
<div class="space-y-2 mb-4">
<label class="block text-sm font-medium">Pacchetti</label>
<div>
<label class="text-xs text-gray-500">APT Package</label>
<input type="text" id="app-apt" class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm">
</div>
<div>
<label class="text-xs text-gray-500">AppStream ID</label>
<input type="text" id="app-appstream" class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm">
</div>
<div>
<label class="text-xs text-gray-500">Flatpak URL</label>
<input type="text" id="app-flatpak" class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm">
</div>
</div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="closeForm()" class="px-4 py-2 text-gray-300 hover:text-white transition">Annulla</button>
<button type="submit" class="px-6 py-2 bg-green-600 hover:bg-green-700 text-white font-bold rounded transition">Salva</button>
</div>
</form>
</div>
</div>
<script src="admin.js"></script>
</body>
</html>

244
frontend/admin.js Normal file
View File

@@ -0,0 +1,244 @@
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();
}
});