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:
14
backend/Dockerfile
Normal file
14
backend/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install --production
|
||||
|
||||
COPY server.js ./server.js
|
||||
COPY data/apps.json ./data/apps.json
|
||||
|
||||
ENV PORT=3000
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
1
backend/data/apps.json
Normal file
1
backend/data/apps.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
54
backend/k8s/admin-deployment.yaml
Normal file
54
backend/k8s/admin-deployment.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: admin-api
|
||||
labels:
|
||||
app: admin-api
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: admin-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: admin-api
|
||||
spec:
|
||||
containers:
|
||||
- name: admin-api
|
||||
image: admin-api:latest
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
- name: ADMIN_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: admin-secrets
|
||||
key: admin-token
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: admin-secrets
|
||||
key: jwt-secret
|
||||
resources:
|
||||
requests:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "200m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/apps
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/apps
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
18
backend/k8s/admin-ingress.yaml
Normal file
18
backend/k8s/admin-ingress.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: admin-api
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- host: admin.apps.local
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: admin-api
|
||||
port:
|
||||
number: 80
|
||||
15
backend/k8s/admin-service.yaml
Normal file
15
backend/k8s/admin-service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: admin-api
|
||||
labels:
|
||||
app: admin-api
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 3000
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app: admin-api
|
||||
16
backend/package.json
Normal file
16
backend/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "linux-app-store-admin",
|
||||
"version": "1.0.0",
|
||||
"description": "Admin panel API for Linux App Store",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"express-jwt": "^8.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"cors": "^2.8.5"
|
||||
}
|
||||
}
|
||||
173
backend/server.js
Normal file
173
backend/server.js
Normal file
@@ -0,0 +1,173 @@
|
||||
const express = require('express');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-key';
|
||||
const APPS_FILE = path.join(__dirname, 'data', 'apps.json');
|
||||
|
||||
app.use(express.json());
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.status(200).end();
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
function verifyToken(req, res, next) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({ error: 'Authorization header required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const token = authHeader.split(' ')[1];
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
req.auth = decoded;
|
||||
next();
|
||||
} catch (err) {
|
||||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
}
|
||||
|
||||
function readAppsFile() {
|
||||
try {
|
||||
if (fs.existsSync(APPS_FILE)) {
|
||||
const data = fs.readFileSync(APPS_FILE, 'utf8');
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error reading apps file:', err);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function writeAppsFile(apps) {
|
||||
try {
|
||||
const tempFile = `${APPS_FILE}.tmp`;
|
||||
fs.writeFileSync(tempFile, JSON.stringify(apps, null, 2));
|
||||
fs.renameSync(tempFile, APPS_FILE);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Error writing apps file:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
app.post('/api/admin/login', (req, res) => {
|
||||
const { token } = req.body;
|
||||
const adminToken = process.env.ADMIN_TOKEN;
|
||||
|
||||
if (!adminToken) {
|
||||
return res.status(500).json({ error: 'ADMIN_TOKEN not configured' });
|
||||
}
|
||||
|
||||
if (token === adminToken) {
|
||||
const payload = {
|
||||
admin: true,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24)
|
||||
};
|
||||
const secret = process.env.JWT_SECRET || 'fallback-secret-key';
|
||||
const apiKey = jwt.sign(payload, secret);
|
||||
return res.json({ apiKey });
|
||||
}
|
||||
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
});
|
||||
|
||||
app.get('/api/apps', (req, res) => {
|
||||
const apps = readAppsFile();
|
||||
return res.json(apps);
|
||||
});
|
||||
|
||||
app.post('/api/admin/apps', verifyToken, (req, res) => {
|
||||
if (!req.auth || !req.auth.admin) {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
const { name, description, icon, colors, packages } = req.body;
|
||||
|
||||
if (!name || !description) {
|
||||
return res.status(400).json({ error: 'Name and description are required' });
|
||||
}
|
||||
|
||||
let apps = readAppsFile();
|
||||
const newId = apps.length > 0 ? Math.max(...apps.map(a => a.id)) + 1 : 1;
|
||||
|
||||
const newApp = {
|
||||
id: newId,
|
||||
name,
|
||||
description,
|
||||
icon: icon || '',
|
||||
colors: colors || { background: '', text: '' },
|
||||
packages: packages || { apt: '', appstream: '', flatpak: '' }
|
||||
};
|
||||
|
||||
apps.push(newApp);
|
||||
|
||||
if (writeAppsFile(apps)) {
|
||||
return res.status(201).json(newApp);
|
||||
}
|
||||
|
||||
return res.status(500).json({ error: 'Failed to save app' });
|
||||
});
|
||||
|
||||
app.put('/api/admin/apps/:id', verifyToken, (req, res) => {
|
||||
if (!req.auth || !req.auth.admin) {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
const { id } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
let apps = readAppsFile();
|
||||
const appIndex = apps.findIndex(a => a.id === parseInt(id));
|
||||
|
||||
if (appIndex === -1) {
|
||||
return res.status(404).json({ error: 'App not found' });
|
||||
}
|
||||
|
||||
const updatedApp = { ...apps[appIndex], ...updates, id: parseInt(id) };
|
||||
apps[appIndex] = updatedApp;
|
||||
|
||||
if (writeAppsFile(apps)) {
|
||||
return res.json(updatedApp);
|
||||
}
|
||||
|
||||
return res.status(500).json({ error: 'Failed to update app' });
|
||||
});
|
||||
|
||||
app.delete('/api/admin/apps/:id', verifyToken, (req, res) => {
|
||||
if (!req.auth || !req.auth.admin) {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
const { id } = req.params;
|
||||
|
||||
let apps = readAppsFile();
|
||||
const appIndex = apps.findIndex(a => a.id === parseInt(id));
|
||||
|
||||
if (appIndex === -1) {
|
||||
return res.status(404).json({ error: 'App not found' });
|
||||
}
|
||||
|
||||
apps.splice(appIndex, 1);
|
||||
|
||||
if (writeAppsFile(apps)) {
|
||||
return res.json({ message: 'App deleted successfully' });
|
||||
}
|
||||
|
||||
return res.status(500).json({ error: 'Failed to delete app' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Admin API running on port ${PORT}`);
|
||||
});
|
||||
24
deploy.sh
24
deploy.sh
@@ -2,17 +2,31 @@
|
||||
set -e
|
||||
|
||||
REGISTRY="git.giaco.net"
|
||||
IMAGE_NAME="capitano/webapplinux"
|
||||
MAIN_IMAGE="capitano/webapplinux"
|
||||
ADMIN_IMAGE="capitano/webapplinux-admin"
|
||||
TAG="latest"
|
||||
|
||||
echo "Building image..."
|
||||
docker build -t $REGISTRY/$IMAGE_NAME:$TAG /home/capitano/kubernetes/infrastructure/webapplinux/
|
||||
echo "Building main image..."
|
||||
docker build -t $REGISTRY/$MAIN_IMAGE:$TAG /home/capitano/kubernetes/infrastructure/webapplinux/
|
||||
|
||||
echo "Pushing to registry..."
|
||||
docker push $REGISTRY/$IMAGE_NAME:$TAG
|
||||
echo "Building admin API image..."
|
||||
docker build -t $REGISTRY/$ADMIN_IMAGE:$TAG /home/capitano/kubernetes/infrastructure/webapplinux/backend/
|
||||
|
||||
echo "Pushing main image..."
|
||||
docker push $REGISTRY/$MAIN_IMAGE:$TAG
|
||||
|
||||
echo "Pushing admin image..."
|
||||
docker push $REGISTRY/$ADMIN_IMAGE:$TAG
|
||||
|
||||
echo "Creating admin secret..."
|
||||
kubectl create secret generic admin-secret -n linuxwebapp \
|
||||
--from-literal=admin-token="${ADMIN_TOKEN:-admin123}" \
|
||||
--from-literal=jwt-secret="${JWT_SECRET:-supersecretjwtkey}" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "Deploying to Kubernetes..."
|
||||
kubectl apply -f k8s/namespace.yaml 2>/dev/null || true
|
||||
kubectl apply -f k8s/
|
||||
|
||||
echo "Check status with: kubectl get pods -n linuxwebapp"
|
||||
echo "Admin panel at: http://admin.apps.local"
|
||||
|
||||
111
frontend/admin.html
Normal file
111
frontend/admin.html
Normal 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
244
frontend/admin.js
Normal 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();
|
||||
}
|
||||
});
|
||||
53
k8s/admin-deployment.yaml
Normal file
53
k8s/admin-deployment.yaml
Normal file
@@ -0,0 +1,53 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: admin-api
|
||||
namespace: linuxwebapp
|
||||
labels:
|
||||
app: admin-api
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: admin-api
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: admin-api
|
||||
spec:
|
||||
containers:
|
||||
- name: admin-api
|
||||
image: git.giaco.net/capitano/webapplinux-admin:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
env:
|
||||
- name: ADMIN_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: admin-secret
|
||||
key: admin-token
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: admin-secret
|
||||
key: jwt-secret
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
22
k8s/admin-ingress.yaml
Normal file
22
k8s/admin-ingress.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: admin-api-ingress
|
||||
namespace: linuxwebapp
|
||||
labels:
|
||||
app: admin-api
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: admin.apps.local
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: admin-api-service
|
||||
port:
|
||||
number: 3000
|
||||
16
k8s/admin-service.yaml
Normal file
16
k8s/admin-service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: admin-api-service
|
||||
namespace: linuxwebapp
|
||||
labels:
|
||||
app: admin-api
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: admin-api
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
protocol: TCP
|
||||
name: admin-api
|
||||
@@ -45,3 +45,38 @@ spec:
|
||||
value: "apps.local"
|
||||
- name: NGINX_PORT
|
||||
value: "80"
|
||||
- name: admin-api
|
||||
image: git.giaco.net/capitano/webapplinux-admin:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
env:
|
||||
- name: ADMIN_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: admin-secret
|
||||
key: admin-token
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: admin-secret
|
||||
key: jwt-secret
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
|
||||
@@ -20,3 +20,27 @@ spec:
|
||||
name: linux-app-store-service
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: admin-api-ingress
|
||||
namespace: linuxwebapp
|
||||
labels:
|
||||
app: admin-api
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: admin.apps.local
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: admin-api-service
|
||||
port:
|
||||
number: 3000
|
||||
|
||||
@@ -14,3 +14,24 @@ spec:
|
||||
targetPort: 80
|
||||
protocol: TCP
|
||||
name: http
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
protocol: TCP
|
||||
name: admin-api
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: admin-api-service
|
||||
namespace: linuxwebapp
|
||||
labels:
|
||||
app: admin-api
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: linux-app-store
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
protocol: TCP
|
||||
name: admin-api
|
||||
|
||||
23
nginx.conf
23
nginx.conf
@@ -1,19 +1,38 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name apps.local;
|
||||
server_name apps.local admin.apps.local;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
index index.html admin.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /admin {
|
||||
alias /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /admin.html;
|
||||
}
|
||||
|
||||
location /admin/ {
|
||||
alias /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /admin.html;
|
||||
}
|
||||
|
||||
location /apps.json {
|
||||
add_header Content-Type application/json;
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://localhost:3000/api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
|
||||
Reference in New Issue
Block a user