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}`);
|
||||
});
|
||||
Reference in New Issue
Block a user