u
This commit is contained in:
256
uiserver/webpack.config.js
Normal file
256
uiserver/webpack.config.js
Normal file
@@ -0,0 +1,256 @@
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const Database = require('better-sqlite3');
|
||||
const { config } = require('dotenv');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// Load env vars
|
||||
config();
|
||||
|
||||
// Database connection for Dev Server API
|
||||
const dbPath = process.env.DB_PATH || path.resolve(__dirname, '../server/data/sensors.db');
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-key-change-me';
|
||||
let db;
|
||||
|
||||
try {
|
||||
db = new Database(dbPath);
|
||||
console.log(`[UI Server] Connected to database at ${dbPath}`);
|
||||
} catch (err) {
|
||||
console.error(`[UI Server] Failed to connect to database at ${dbPath}:`, err.message);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
clean: true,
|
||||
},
|
||||
mode: 'development',
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx'],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: './public/index.html',
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
port: 3905,
|
||||
historyApiFallback: true,
|
||||
hot: true,
|
||||
allowedHosts: 'all',
|
||||
setupMiddlewares: (middlewares, devServer) => {
|
||||
if (!devServer) {
|
||||
throw new Error('webpack-dev-server is not defined');
|
||||
}
|
||||
|
||||
// API Endpoints
|
||||
const app = devServer.app;
|
||||
const bodyParser = require('body-parser');
|
||||
app.use(bodyParser.json());
|
||||
|
||||
// --- Auth API ---
|
||||
app.post('/api/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
try {
|
||||
const stmt = db.prepare('SELECT * FROM users WHERE username = ?');
|
||||
const user = stmt.get(username);
|
||||
|
||||
if (!user || !bcrypt.compareSync(password, user.password_hash)) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const token = jwt.sign({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
}, JWT_SECRET, { expiresIn: '24h' });
|
||||
|
||||
res.json({ token, role: user.role, username: user.username });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Middleware to check auth (Optional for read, required for write)
|
||||
const checkAuth = (req, res, next) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader) {
|
||||
const token = authHeader.split(' ')[1];
|
||||
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||
if (user) req.user = user;
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.user || req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
app.use('/api/views', checkAuth);
|
||||
|
||||
// --- Views API ---
|
||||
app.post('/api/views', requireAdmin, (req, res) => {
|
||||
const { name, config } = req.body;
|
||||
try {
|
||||
const stmt = db.prepare('INSERT INTO views (name, config, created_by) VALUES (?, ?, ?)');
|
||||
const info = stmt.run(name, JSON.stringify(config), req.user.id);
|
||||
res.json({ id: info.lastInsertRowid, name, config });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Publicly list views
|
||||
app.get('/api/views', (req, res) => {
|
||||
try {
|
||||
const stmt = db.prepare('SELECT id, name, created_at FROM views ORDER BY name');
|
||||
const rows = stmt.all();
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/views/:id', (req, res) => {
|
||||
try {
|
||||
const stmt = db.prepare('SELECT * FROM views WHERE id = ?');
|
||||
const view = stmt.get(req.params.id);
|
||||
if (view) {
|
||||
view.config = JSON.parse(view.config);
|
||||
res.json(view);
|
||||
} else {
|
||||
res.status(404).json({ error: 'View not found' });
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete View
|
||||
app.delete('/api/views/:id', requireAdmin, (req, res) => {
|
||||
try {
|
||||
const stmt = db.prepare('DELETE FROM views WHERE id = ?');
|
||||
const info = stmt.run(req.params.id);
|
||||
if (info.changes > 0) {
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(404).json({ error: 'View not found' });
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Update View
|
||||
app.put('/api/views/:id', requireAdmin, (req, res) => {
|
||||
const { name, config } = req.body;
|
||||
try {
|
||||
const stmt = db.prepare('UPDATE views SET name = ?, config = ? WHERE id = ?');
|
||||
const info = stmt.run(name, JSON.stringify(config), req.params.id);
|
||||
if (info.changes > 0) {
|
||||
res.json({ id: req.params.id, name, config });
|
||||
} else {
|
||||
res.status(404).json({ error: 'View not found' });
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/devices
|
||||
// Returns list of unique device/channel pairs
|
||||
app.get('/api/devices', (req, res) => {
|
||||
try {
|
||||
if (!db) throw new Error('Database not connected');
|
||||
// Filter to only numeric channels which can be charted
|
||||
const stmt = db.prepare("SELECT DISTINCT device, channel FROM sensor_events WHERE data_type = 'number' ORDER BY device, channel");
|
||||
const rows = stmt.all();
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/readings
|
||||
// Query params: devices (comma sep), channels (comma sep), since (timestamp)
|
||||
// Actually, user wants "Last 24h".
|
||||
// We can accept `since` or valid ISO string.
|
||||
// Expected params: `?device=x&channel=y` (single) or query for multiple?
|
||||
// User asked for "chart that is refreshed once a minute... display the last 24 hours with the devices/channels previously selected"
|
||||
// Efficient query: select * from sensor_events where timestamp > ? and (device,channel) IN (...)
|
||||
// For simplicity, let's allow fetching by multiple devices/channels or just all for last 24h and filter client side?
|
||||
// No, database filtering is better.
|
||||
// Let's support ?since=ISO_DATE
|
||||
|
||||
app.get('/api/readings', (req, res) => {
|
||||
try {
|
||||
if (!db) throw new Error('Database not connected');
|
||||
const { since } = req.query;
|
||||
|
||||
let sql = 'SELECT * FROM sensor_events WHERE timestamp > ? ';
|
||||
const params = [since || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()];
|
||||
|
||||
// Helper for filtering could be added here if needed,
|
||||
// but fetching last 24h of *all* data might not be too huge if not too many sensors.
|
||||
// However, optimization: if query params `channels` provided as "device:channel,device2:channel2"
|
||||
|
||||
if (req.query.selection) {
|
||||
// selection format: "device:channel,device:channel"
|
||||
const selections = req.query.selection.split(',');
|
||||
if (selections.length > 0) {
|
||||
const placeholders = selections.map(() => '(device = ? AND channel = ?)').join(' OR ');
|
||||
sql += `AND (${placeholders}) `;
|
||||
selections.forEach(s => {
|
||||
const lastColonIndex = s.lastIndexOf(':');
|
||||
if (lastColonIndex !== -1) {
|
||||
const d = s.substring(0, lastColonIndex);
|
||||
const c = s.substring(lastColonIndex + 1);
|
||||
params.push(d, c);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sql += 'ORDER BY timestamp ASC';
|
||||
|
||||
const stmt = db.prepare(sql);
|
||||
const rows = stmt.all(...params);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
return middlewares;
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user