- Updated server.js to serve admin.html on GET / - Fixed path to admin.html in Docker container - Admin panel now accessible at http://<ip>:3000
180 lines
4.7 KiB
JavaScript
180 lines
4.7 KiB
JavaScript
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');
|
|
|
|
// Serve admin frontend
|
|
const adminPath = path.join(__dirname, 'admin.html');
|
|
app.get('/', (req, res) => {
|
|
res.sendFile(adminPath);
|
|
});
|
|
|
|
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}`);
|
|
});
|