添加鉴权逻辑
This commit is contained in:
145
server/index.js
145
server/index.js
@@ -11,24 +11,80 @@ const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
let isDbInitialized = false;
|
||||
const sessions = new Map(); // Simple session store: sessionId -> {userId, username}
|
||||
|
||||
// Middleware: Check Auth
|
||||
function requireAuth(req, res, next) {
|
||||
const sessionId = getCookie(req, 'session_id');
|
||||
if (sessionId && sessions.has(sessionId)) {
|
||||
req.user = sessions.get(sessionId);
|
||||
return next();
|
||||
}
|
||||
res.status(401).json({ error: 'Auth required' });
|
||||
}
|
||||
|
||||
// Helper: Get Cookie
|
||||
function getCookie(req, name) {
|
||||
const matches = req.headers.cookie && req.headers.cookie.match(new RegExp('(?:^|; )' + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + '=([^;]*)'));
|
||||
return matches ? decodeURIComponent(matches[1]) : undefined;
|
||||
}
|
||||
|
||||
async function checkDb() {
|
||||
try {
|
||||
const [rows] = await db.query("SHOW TABLES LIKE 'prometheus_sources'");
|
||||
if (rows.length > 0) {
|
||||
isDbInitialized = true;
|
||||
} else {
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(path.join(__dirname, '..', '.env'))) {
|
||||
isDbInitialized = false;
|
||||
return;
|
||||
}
|
||||
const [rows] = await db.query("SHOW TABLES LIKE 'prometheus_sources'");
|
||||
isDbInitialized = rows.length > 0;
|
||||
} catch (err) {
|
||||
isDbInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
checkDb();
|
||||
// --- Auth API ---
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
try {
|
||||
const [rows] = await db.query('SELECT * FROM users WHERE username = ?', [username]);
|
||||
if (rows.length === 0) return res.status(401).json({ error: 'Invalid credentials' });
|
||||
|
||||
const user = rows[0];
|
||||
const hash = crypto.pbkdf2Sync(password, user.salt, 1000, 64, 'sha512').toString('hex');
|
||||
|
||||
if (hash === user.password) {
|
||||
const sessionId = crypto.randomBytes(32).toString('hex');
|
||||
sessions.set(sessionId, { id: user.id, username: user.username });
|
||||
res.setHeader('Set-Cookie', `session_id=${sessionId}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`);
|
||||
res.json({ success: true, username: user.username });
|
||||
} else {
|
||||
res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Login failed' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/logout', (req, res) => {
|
||||
const sessionId = getCookie(req, 'session_id');
|
||||
if (sessionId) sessions.delete(sessionId);
|
||||
res.setHeader('Set-Cookie', 'session_id=; Path=/; HttpOnly; Max-Age=0');
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.get('/api/auth/status', (req, res) => {
|
||||
const sessionId = getCookie(req, 'session_id');
|
||||
if (sessionId && sessions.has(sessionId)) {
|
||||
res.json({ authenticated: true, username: sessions.get(sessionId).username });
|
||||
} else {
|
||||
res.json({ authenticated: false });
|
||||
}
|
||||
});
|
||||
|
||||
// Setup API Routes
|
||||
app.post('/api/setup/test', async (req, res) => {
|
||||
@@ -78,6 +134,16 @@ app.post('/api/setup/init', async (req, res) => {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
salt VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
|
||||
await connection.end();
|
||||
|
||||
// Save to .env
|
||||
@@ -110,18 +176,71 @@ REFRESH_INTERVAL=${process.env.REFRESH_INTERVAL || 5000}
|
||||
}
|
||||
});
|
||||
|
||||
// Middleware to protect routes
|
||||
app.use((req, res, next) => {
|
||||
if (!isDbInitialized) {
|
||||
if (req.path.startsWith('/api/setup') || req.path === '/init.html' || req.path.startsWith('/css/') || req.path.startsWith('/js/') || req.path.startsWith('/fonts/')) {
|
||||
return next();
|
||||
// Setup Status Check
|
||||
app.get('/api/setup/status', async (req, res) => {
|
||||
try {
|
||||
if (!isDbInitialized) {
|
||||
return res.json({ initialized: false, step: 'db' });
|
||||
}
|
||||
const [rows] = await db.query('SELECT COUNT(*) as count FROM users');
|
||||
if (rows[0].count === 0) {
|
||||
return res.json({ initialized: true, needsAdmin: true, step: 'admin' });
|
||||
}
|
||||
res.json({ initialized: true, needsAdmin: false, step: 'prom' });
|
||||
} catch (err) {
|
||||
console.error('Status check error:', err);
|
||||
res.json({ initialized: false, step: 'db' });
|
||||
}
|
||||
});
|
||||
|
||||
// Create First Admin
|
||||
app.post('/api/setup/admin', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (!username || !password) return res.status(400).json({ error: 'Username and password are required' });
|
||||
|
||||
try {
|
||||
const [rows] = await db.query('SELECT COUNT(*) as count FROM users');
|
||||
if (rows[0].count > 0) return res.status(403).json({ error: 'Admin already exists' });
|
||||
|
||||
const salt = crypto.randomBytes(16).toString('hex');
|
||||
const hash = crypto.pbkdf2Sync(password, salt, 1000, 64, 'sha512').toString('hex');
|
||||
|
||||
await db.query('INSERT INTO users (username, password, salt) VALUES (?, ?, ?)', [username, hash, salt]);
|
||||
res.json({ success: true, message: 'Admin account created' });
|
||||
} catch (err) {
|
||||
console.error('Admin creation error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Middleware to protect routes & enforce setup
|
||||
app.use(async (req, res, next) => {
|
||||
// Allow system files and setup APIs
|
||||
if (req.path.startsWith('/api/setup') || req.path === '/init.html' || req.path.startsWith('/css/') || req.path.startsWith('/js/') || req.path.startsWith('/fonts/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Enforce DB setup
|
||||
if (!isDbInitialized) {
|
||||
if (req.path.startsWith('/api/')) {
|
||||
return res.status(503).json({ error: 'Database not initialized', needSetup: true });
|
||||
}
|
||||
return res.redirect('/init.html');
|
||||
}
|
||||
|
||||
// Enforce User setup
|
||||
try {
|
||||
const [rows] = await db.query('SELECT COUNT(*) as count FROM users');
|
||||
if (rows[0].count === 0) {
|
||||
if (req.path.startsWith('/api/')) {
|
||||
return res.status(503).json({ error: 'Admin not configured', needAdmin: true });
|
||||
}
|
||||
return res.redirect('/init.html?step=admin');
|
||||
}
|
||||
} catch (err) {
|
||||
// If table doesn't exist, it's a DB initialization issue
|
||||
}
|
||||
|
||||
if (req.path === '/init.html') {
|
||||
return res.redirect('/');
|
||||
}
|
||||
@@ -153,7 +272,7 @@ app.get('/api/sources', async (req, res) => {
|
||||
});
|
||||
|
||||
// Add a new Prometheus source
|
||||
app.post('/api/sources', async (req, res) => {
|
||||
app.post('/api/sources', requireAuth, async (req, res) => {
|
||||
let { name, url, description } = req.body;
|
||||
if (!name || !url) {
|
||||
return res.status(400).json({ error: 'Name and URL are required' });
|
||||
@@ -173,7 +292,7 @@ app.post('/api/sources', async (req, res) => {
|
||||
});
|
||||
|
||||
// Update a Prometheus source
|
||||
app.put('/api/sources/:id', async (req, res) => {
|
||||
app.put('/api/sources/:id', requireAuth, async (req, res) => {
|
||||
let { name, url, description } = req.body;
|
||||
if (url && !/^https?:\/\//i.test(url)) url = 'http://' + url;
|
||||
try {
|
||||
@@ -190,7 +309,7 @@ app.put('/api/sources/:id', async (req, res) => {
|
||||
});
|
||||
|
||||
// Delete a Prometheus source
|
||||
app.delete('/api/sources/:id', async (req, res) => {
|
||||
app.delete('/api/sources/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
await db.query('DELETE FROM prometheus_sources WHERE id = ?', [req.params.id]);
|
||||
res.json({ message: 'Source deleted' });
|
||||
|
||||
@@ -24,6 +24,18 @@ async function initDatabase() {
|
||||
|
||||
await connection.query(`USE \`${dbName}\``);
|
||||
|
||||
// Create users table
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
salt VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log(' ✅ Table "users" ready');
|
||||
|
||||
// Create prometheus_sources table
|
||||
await connection.query(`
|
||||
CREATE TABLE IF NOT EXISTS prometheus_sources (
|
||||
@@ -32,8 +44,8 @@ async function initDatabase() {
|
||||
url VARCHAR(500) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
`);
|
||||
console.log(' ✅ Table "prometheus_sources" ready');
|
||||
|
||||
|
||||
@@ -178,10 +178,12 @@ async function getOverviewMetrics(url, sourceName) {
|
||||
// Build per-instance data map
|
||||
const instances = new Map();
|
||||
|
||||
const getOrCreate = (instance) => {
|
||||
if (!instances.has(instance)) {
|
||||
instances.set(instance, {
|
||||
instance,
|
||||
const getOrCreate = (metric) => {
|
||||
const key = metric.instance;
|
||||
if (!instances.has(key)) {
|
||||
instances.set(key, {
|
||||
instance: key,
|
||||
job: metric.job || 'Unknown',
|
||||
source: sourceName,
|
||||
cpuPercent: 0,
|
||||
cpuCores: 0,
|
||||
@@ -194,54 +196,54 @@ async function getOverviewMetrics(url, sourceName) {
|
||||
up: false
|
||||
});
|
||||
}
|
||||
return instances.get(instance);
|
||||
return instances.get(key);
|
||||
};
|
||||
|
||||
// Parse UP status
|
||||
for (const r of upResult) {
|
||||
const inst = getOrCreate(r.metric.instance);
|
||||
const inst = getOrCreate(r.metric);
|
||||
inst.up = parseFloat(r.value[1]) === 1;
|
||||
}
|
||||
|
||||
// Parse CPU usage
|
||||
for (const r of cpuResult) {
|
||||
const inst = getOrCreate(r.metric.instance);
|
||||
const inst = getOrCreate(r.metric);
|
||||
inst.cpuPercent = parseFloat(r.value[1]) || 0;
|
||||
}
|
||||
|
||||
// Parse CPU count
|
||||
for (const r of cpuCountResult) {
|
||||
const inst = getOrCreate(r.metric.instance);
|
||||
const inst = getOrCreate(r.metric);
|
||||
inst.cpuCores = parseFloat(r.value[1]) || 0;
|
||||
}
|
||||
|
||||
// Parse memory
|
||||
for (const r of memTotalResult) {
|
||||
const inst = getOrCreate(r.metric.instance);
|
||||
const inst = getOrCreate(r.metric);
|
||||
inst.memTotal = parseFloat(r.value[1]) || 0;
|
||||
}
|
||||
for (const r of memAvailResult) {
|
||||
const inst = getOrCreate(r.metric.instance);
|
||||
const inst = getOrCreate(r.metric);
|
||||
inst.memUsed = inst.memTotal - (parseFloat(r.value[1]) || 0);
|
||||
}
|
||||
|
||||
// Parse disk
|
||||
for (const r of diskTotalResult) {
|
||||
const inst = getOrCreate(r.metric.instance);
|
||||
const inst = getOrCreate(r.metric);
|
||||
inst.diskTotal = parseFloat(r.value[1]) || 0;
|
||||
}
|
||||
for (const r of diskFreeResult) {
|
||||
const inst = getOrCreate(r.metric.instance);
|
||||
const inst = getOrCreate(r.metric);
|
||||
inst.diskUsed = inst.diskTotal - (parseFloat(r.value[1]) || 0);
|
||||
}
|
||||
|
||||
// Parse network rates
|
||||
for (const r of netRxResult) {
|
||||
const inst = getOrCreate(r.metric.instance);
|
||||
const inst = getOrCreate(r.metric);
|
||||
inst.netRx = parseFloat(r.value[1]) || 0;
|
||||
}
|
||||
for (const r of netTxResult) {
|
||||
const inst = getOrCreate(r.metric.instance);
|
||||
const inst = getOrCreate(r.metric);
|
||||
inst.netTx = parseFloat(r.value[1]) || 0;
|
||||
}
|
||||
|
||||
@@ -290,7 +292,8 @@ async function getOverviewMetrics(url, sourceName) {
|
||||
},
|
||||
network: {
|
||||
rx: totalNetRx,
|
||||
tx: totalNetTx
|
||||
tx: totalNetTx,
|
||||
total: totalNetRx + totalNetTx
|
||||
},
|
||||
traffic24h: {
|
||||
rx: totalTraffic24hRx,
|
||||
|
||||
Reference in New Issue
Block a user