添加鉴权逻辑

This commit is contained in:
CN-JS-HuiBai
2026-04-04 17:49:00 +08:00
parent a2a477d3fb
commit 2bad8978a4
8 changed files with 474 additions and 57 deletions

View File

@@ -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' });

View File

@@ -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');

View File

@@ -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,