Refactor database credentials and user permissions; update UI components for better usability and accessibility

- Changed default MariaDB password in docker-compose.yml from 'SE3112' to '3112'.
- Added user creation and permissions for 'wago' in the MariaDB initialization script.
- Updated unit options in App.jsx to include a digital/bool option.
- Enhanced LoginScreen component to support inline display and improved error handling.
- Improved point filtering logic in PointLibrary and adjusted default unit handling.
- Refined authentication checks for dashboard and admin permissions.
- Simplified API calls by removing unnecessary token checks.
- Enhanced TrendChart component with better data handling and formatting.
- Updated CSS styles for improved layout and responsiveness in trend views and sidebar.
This commit is contained in:
jhartworks
2026-07-01 11:58:42 +02:00
parent e34626f15b
commit 2b9ff12a9f
11 changed files with 412 additions and 279 deletions
+19
View File
@@ -32,6 +32,25 @@ export function authenticateToken(req, res, next) {
}
}
export function authenticateTokenOptional(req, _res, next) {
const header = req.headers.authorization || "";
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
if (!token) {
req.user = null;
next();
return;
}
try {
req.user = jwt.verify(token, config.jwtSecret);
} catch {
req.user = null;
}
next();
}
export function requireRoles(...roles) {
return (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
+29 -12
View File
@@ -1,7 +1,7 @@
import bcrypt from "bcryptjs";
import cors from "cors";
import express from "express";
import { authenticateToken, createToken, requireRoles } from "./auth.js";
import { authenticateToken, authenticateTokenOptional, createToken, requireRoles } from "./auth.js";
import { config } from "./config.js";
import { pool } from "./db.js";
import {
@@ -88,7 +88,7 @@ function assertPoints(points) {
}
function canUseSelection(selection, user) {
return Boolean(selection) && (selection.shared || selection.ownerId === user.sub || user.role === "admin");
return Boolean(selection) && (selection.shared || (Boolean(user) && (selection.ownerId === user.sub || user.role === "admin")));
}
function slugifyUsername(value) {
@@ -147,6 +147,16 @@ async function serializeUserWithPermissions(row) {
};
}
const GUEST_VIEWER = {
sub: "public",
role: "viewer",
name: "Gast",
};
function resolveViewerUser(req) {
return req.user || GUEST_VIEWER;
}
function requirePermission(...permissions) {
return async (req, res, next) => {
try {
@@ -218,6 +228,12 @@ async function ensureUtf8Database() {
}
}
async function ensureWagoApplicationUser() {
await pool.query("CREATE USER IF NOT EXISTS 'wago'@'%' IDENTIFIED BY '3112'");
await pool.query(`GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON \`${config.dbName}\`.* TO 'wago'@'%'`);
await pool.query("FLUSH PRIVILEGES");
}
async function ensureUserSchema() {
await pool.query(
`
@@ -384,7 +400,7 @@ async function resolveIspPointCount(isp) {
return await resolveTrendTablePointCount(pool, config.dbName, isp);
}
app.get("/api/isps", authenticateToken, wrap(async (_req, res) => {
app.get("/api/isps", authenticateTokenOptional, wrap(async (_req, res) => {
const isps = await getAvailableIsps(pool, config.dbName);
const response = await Promise.all(
isps.map(async (isp) => {
@@ -400,7 +416,7 @@ app.get("/api/isps", authenticateToken, wrap(async (_req, res) => {
res.json({ isps: response });
}));
app.get("/api/isps/:isp/aliases", authenticateToken, wrap(async (req, res) => {
app.get("/api/isps/:isp/aliases", authenticateTokenOptional, wrap(async (req, res) => {
const pointCount = await resolveIspPointCount(req.params.isp);
const metadata = await ensureIspMetadata(pool, redis, req.params.isp, pointCount);
res.json(metadata);
@@ -694,20 +710,20 @@ app.delete("/api/selections/:selectionId", authenticateToken, requirePermission(
res.status(204).send();
}));
app.post("/api/trends/query", authenticateToken, requirePermission("view_trends"), wrap(async (req, res) => {
const points = await resolveSelectionPoints(req.body, req.user);
app.post("/api/trends/query", authenticateTokenOptional, wrap(async (req, res) => {
const points = await resolveSelectionPoints(req.body, resolveViewerUser(req));
const trendData = await fetchTrendSeries(pool, redis, points, normalizeRange(req.body.range));
res.json(trendData);
}));
app.post("/api/points/latest", authenticateToken, requirePermission("view_trends"), wrap(async (req, res) => {
const points = await resolveSelectionPoints(req.body, req.user);
app.post("/api/points/latest", authenticateTokenOptional, wrap(async (req, res) => {
const points = await resolveSelectionPoints(req.body, resolveViewerUser(req));
const values = await fetchLatestValues(pool, redis, points);
res.json({ values });
}));
app.get("/api/dashboard", authenticateToken, requirePermission("view_dashboard"), wrap(async (req, res) => {
const dashboard = await getDashboard(redis, req.user.sub);
app.get("/api/dashboard", authenticateTokenOptional, wrap(async (req, res) => {
const dashboard = await getDashboard(redis, req.user?.sub || "public");
res.json(dashboard);
}));
@@ -716,12 +732,13 @@ app.put("/api/dashboard", authenticateToken, requirePermission("view_dashboard")
res.json(dashboard);
}));
app.post("/api/dashboard/data", authenticateToken, requirePermission("view_dashboard"), wrap(async (req, res) => {
app.post("/api/dashboard/data", authenticateTokenOptional, wrap(async (req, res) => {
const widgets = Array.isArray(req.body.widgets) ? req.body.widgets : [];
const results = [];
const viewer = resolveViewerUser(req);
for (const widget of widgets) {
const points = widget.selectionId ? await resolveSelectionPoints({ selectionId: widget.selectionId }, req.user) : assertPoints(widget.points || []);
const points = widget.selectionId ? await resolveSelectionPoints({ selectionId: widget.selectionId }, viewer) : assertPoints(widget.points || []);
if (widget.type === "chart") {
const trendData = await fetchTrendSeries(pool, redis, points, normalizeRange(widget.range));
+14 -9
View File
@@ -113,14 +113,19 @@ function createRandomPointColor(seed) {
return hslToHex(hue, saturation, lightness);
}
export function normalizeEngineeringUnit(value) {
export function normalizeEngineeringUnit(value, kind = "analog") {
const normalizedKind = kind === "digital" ? "digital" : "analog";
const raw = String(value ?? "").trim();
if (!raw) {
return "\u00B0C";
return normalizedKind === "digital" ? "bool" : "\u00B0C";
}
if (normalizedKind === "digital" && /^(?:bool|boolean)$/i.test(raw)) {
return "bool";
}
if (/^\?C$/i.test(raw) || /^\u00B0\s*C$/i.test(raw) || (/[^\x00-\x7F]/.test(raw) && /C/i.test(raw))) {
return "\u00B0C";
return normalizedKind === "digital" ? "bool" : "\u00B0C";
}
return raw;
@@ -388,7 +393,7 @@ export async function ensureIspMetadata(pool, redis, isp, pointCount = LEGACY_PO
parsed.points[pointKey] = {
...defaultPoint,
alias: initialAlias,
unit: normalizeEngineeringUnit(sourceSeed?.unit || defaultPoint.unit),
unit: normalizeEngineeringUnit(sourceSeed?.unit || defaultPoint.unit, sourceSeed?.kind || defaultPoint.kind),
kind: sourceSeed?.kind || defaultPoint.kind,
};
changed = true;
@@ -407,13 +412,13 @@ export async function ensureIspMetadata(pool, redis, isp, pointCount = LEGACY_PO
point.kind = sourceSeed.kind;
changed = true;
}
const normalizedUnit = normalizeEngineeringUnit(point.unit);
const normalizedUnit = normalizeEngineeringUnit(point.unit, point.kind || sourceSeed?.kind || defaultPoint.kind);
if (normalizedUnit !== point.unit) {
point.unit = normalizedUnit;
changed = true;
}
if ((!point.unit || point.unit === normalizeEngineeringUnit(defaultPoint.unit)) && sourceSeed?.unit) {
const seededUnit = normalizeEngineeringUnit(sourceSeed.unit);
if ((!point.unit || point.unit === normalizeEngineeringUnit(defaultPoint.unit, defaultPoint.kind)) && sourceSeed?.unit) {
const seededUnit = normalizeEngineeringUnit(sourceSeed.unit, sourceSeed.kind || point.kind || defaultPoint.kind);
if (point.unit !== seededUnit) {
point.unit = seededUnit;
changed = true;
@@ -463,7 +468,7 @@ export async function ensureIspMetadata(pool, redis, isp, pointCount = LEGACY_PO
metadata.points[pointKey] = {
...metadata.points[pointKey],
alias: sourceSeed.alias || sourceSeed.name || metadata.points[pointKey].alias,
unit: normalizeEngineeringUnit(sourceSeed.unit || metadata.points[pointKey].unit),
unit: normalizeEngineeringUnit(sourceSeed.unit || metadata.points[pointKey].unit, sourceSeed.kind || metadata.points[pointKey].kind),
kind: sourceSeed.kind || metadata.points[pointKey].kind,
};
});
@@ -506,7 +511,7 @@ export async function getPointDescriptors(pool, redis, points) {
key: buildPointKey(isp, pointIndex),
displayName: metadata.displayName,
alias: pointMeta.alias,
unit: normalizeEngineeringUnit(pointMeta.unit),
unit: normalizeEngineeringUnit(pointMeta.unit, pointMeta.kind),
factor: pointMeta.factor,
kind: pointMeta.kind,
min: pointMeta.min,