mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-27 10:03:45 +00:00
913 lines
26 KiB
JavaScript
913 lines
26 KiB
JavaScript
require("dotenv").config();
|
||
const express = require("express");
|
||
const cors = require("cors");
|
||
const helmet = require("helmet");
|
||
const rateLimit = require("express-rate-limit");
|
||
const {
|
||
createPrismaClient,
|
||
waitForDatabase,
|
||
disconnectPrisma,
|
||
} = require("./config/database");
|
||
const winston = require("winston");
|
||
|
||
// Import routes
|
||
const authRoutes = require("./routes/authRoutes");
|
||
const hostRoutes = require("./routes/hostRoutes");
|
||
const hostGroupRoutes = require("./routes/hostGroupRoutes");
|
||
const packageRoutes = require("./routes/packageRoutes");
|
||
const dashboardRoutes = require("./routes/dashboardRoutes");
|
||
const permissionsRoutes = require("./routes/permissionsRoutes");
|
||
const settingsRoutes = require("./routes/settingsRoutes");
|
||
const {
|
||
router: dashboardPreferencesRoutes,
|
||
} = require("./routes/dashboardPreferencesRoutes");
|
||
const repositoryRoutes = require("./routes/repositoryRoutes");
|
||
const versionRoutes = require("./routes/versionRoutes");
|
||
const tfaRoutes = require("./routes/tfaRoutes");
|
||
const updateScheduler = require("./services/updateScheduler");
|
||
const { initSettings } = require("./services/settingsService");
|
||
|
||
// Initialize Prisma client with optimized connection pooling for multiple instances
|
||
const prisma = createPrismaClient();
|
||
|
||
// Simple version comparison function for semantic versioning
|
||
function compareVersions(version1, version2) {
|
||
const v1Parts = version1.split(".").map(Number);
|
||
const v2Parts = version2.split(".").map(Number);
|
||
|
||
// Ensure both arrays have the same length
|
||
const maxLength = Math.max(v1Parts.length, v2Parts.length);
|
||
while (v1Parts.length < maxLength) v1Parts.push(0);
|
||
while (v2Parts.length < maxLength) v2Parts.push(0);
|
||
|
||
for (let i = 0; i < maxLength; i++) {
|
||
if (v1Parts[i] > v2Parts[i]) return true;
|
||
if (v1Parts[i] < v2Parts[i]) return false;
|
||
}
|
||
|
||
return false; // versions are equal
|
||
}
|
||
|
||
// Function to check and import agent version on startup
|
||
async function checkAndImportAgentVersion() {
|
||
console.log("🔍 Starting agent version auto-import check...");
|
||
|
||
// Skip if auto-import is disabled
|
||
if (process.env.AUTO_IMPORT_AGENT_VERSION === "false") {
|
||
console.log("❌ Auto-import of agent version is disabled");
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info("Auto-import of agent version is disabled");
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const fs = require("node:fs");
|
||
const path = require("node:path");
|
||
const crypto = require("node:crypto");
|
||
|
||
// Read and validate agent script
|
||
const agentScriptPath = path.join(
|
||
__dirname,
|
||
"../../agents/patchmon-agent.sh",
|
||
);
|
||
console.log("📁 Agent script path:", agentScriptPath);
|
||
|
||
// Check if file exists
|
||
if (!fs.existsSync(agentScriptPath)) {
|
||
console.log("❌ Agent script file not found, skipping version check");
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.warn("Agent script file not found, skipping version check");
|
||
}
|
||
return;
|
||
}
|
||
|
||
console.log("✅ Agent script file found");
|
||
|
||
// Read the file content
|
||
const scriptContent = fs.readFileSync(agentScriptPath, "utf8");
|
||
|
||
// Extract version from script content
|
||
const versionMatch = scriptContent.match(/AGENT_VERSION="([^"]+)"/);
|
||
|
||
if (!versionMatch) {
|
||
console.log(
|
||
"❌ Could not extract version from agent script, skipping version check",
|
||
);
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.warn(
|
||
"Could not extract version from agent script, skipping version check",
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const localVersion = versionMatch[1];
|
||
console.log("📋 Local version:", localVersion);
|
||
|
||
// Check if this version already exists in database
|
||
const existingVersion = await prisma.agent_versions.findUnique({
|
||
where: { version: localVersion },
|
||
});
|
||
|
||
if (existingVersion) {
|
||
console.log(
|
||
`✅ Agent version ${localVersion} already exists in database`,
|
||
);
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info(`Agent version ${localVersion} already exists in database`);
|
||
}
|
||
return;
|
||
}
|
||
|
||
console.log(`🆕 Agent version ${localVersion} not found in database`);
|
||
|
||
// Get existing versions for comparison
|
||
const allVersions = await prisma.agent_versions.findMany({
|
||
select: { version: true },
|
||
orderBy: { created_at: "desc" },
|
||
});
|
||
|
||
// Determine version flags and whether to proceed
|
||
const isFirstVersion = allVersions.length === 0;
|
||
const isNewerVersion =
|
||
!isFirstVersion && compareVersions(localVersion, allVersions[0].version);
|
||
|
||
if (!isFirstVersion && !isNewerVersion) {
|
||
console.log(
|
||
`❌ Agent version ${localVersion} is not newer than existing versions, skipping import`,
|
||
);
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info(
|
||
`Agent version ${localVersion} is not newer than existing versions, skipping import`,
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const shouldSetAsCurrent = isFirstVersion || isNewerVersion;
|
||
const shouldSetAsDefault = isFirstVersion;
|
||
|
||
console.log(
|
||
isFirstVersion
|
||
? `📊 No existing versions found in database`
|
||
: `📊 Found ${allVersions.length} existing versions in database, latest: ${allVersions[0].version}`,
|
||
);
|
||
|
||
if (!isFirstVersion) {
|
||
console.log(
|
||
`🔄 Version comparison: ${localVersion} > ${allVersions[0].version} = ${isNewerVersion}`,
|
||
);
|
||
}
|
||
|
||
// Clear existing flags if needed
|
||
const updatePromises = [];
|
||
if (shouldSetAsCurrent) {
|
||
updatePromises.push(
|
||
prisma.agent_versions.updateMany({
|
||
where: { is_current: true },
|
||
data: { is_current: false },
|
||
}),
|
||
);
|
||
}
|
||
if (shouldSetAsDefault) {
|
||
updatePromises.push(
|
||
prisma.agent_versions.updateMany({
|
||
where: { is_default: true },
|
||
data: { is_default: false },
|
||
}),
|
||
);
|
||
}
|
||
|
||
if (updatePromises.length > 0) {
|
||
await Promise.all(updatePromises);
|
||
}
|
||
|
||
// Create new version
|
||
await prisma.agent_versions.create({
|
||
data: {
|
||
id: crypto.randomUUID(),
|
||
version: localVersion,
|
||
release_notes: `Auto-imported on startup (${new Date().toISOString()})`,
|
||
script_content: scriptContent,
|
||
is_default: shouldSetAsDefault,
|
||
is_current: shouldSetAsCurrent,
|
||
updated_at: new Date(),
|
||
},
|
||
});
|
||
|
||
console.log(
|
||
`🎉 Successfully auto-imported new agent version ${localVersion} on startup`,
|
||
);
|
||
if (shouldSetAsCurrent) {
|
||
console.log(`✅ Set version ${localVersion} as current version`);
|
||
}
|
||
if (shouldSetAsDefault) {
|
||
console.log(`✅ Set version ${localVersion} as default version`);
|
||
}
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info(
|
||
`✅ Auto-imported new agent version ${localVersion} on startup (current: ${shouldSetAsCurrent}, default: ${shouldSetAsDefault})`,
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error(
|
||
"❌ Failed to check/import agent version on startup:",
|
||
error.message,
|
||
);
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.error(
|
||
"Failed to check/import agent version on startup:",
|
||
error.message,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Function to check and create default role permissions on startup
|
||
async function checkAndCreateRolePermissions() {
|
||
console.log("🔐 Starting role permissions auto-creation check...");
|
||
|
||
// Skip if auto-creation is disabled
|
||
if (process.env.AUTO_CREATE_ROLE_PERMISSIONS === "false") {
|
||
console.log("❌ Auto-creation of role permissions is disabled");
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info("Auto-creation of role permissions is disabled");
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const crypto = require("node:crypto");
|
||
|
||
// Define default roles and permissions
|
||
const defaultRoles = [
|
||
{
|
||
id: crypto.randomUUID(),
|
||
role: "admin",
|
||
can_view_dashboard: true,
|
||
can_view_hosts: true,
|
||
can_manage_hosts: true,
|
||
can_view_packages: true,
|
||
can_manage_packages: true,
|
||
can_view_users: true,
|
||
can_manage_users: true,
|
||
can_view_reports: true,
|
||
can_export_data: true,
|
||
can_manage_settings: true,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
},
|
||
{
|
||
id: crypto.randomUUID(),
|
||
role: "user",
|
||
can_view_dashboard: true,
|
||
can_view_hosts: true,
|
||
can_manage_hosts: false,
|
||
can_view_packages: true,
|
||
can_manage_packages: false,
|
||
can_view_users: false,
|
||
can_manage_users: false,
|
||
can_view_reports: true,
|
||
can_export_data: false,
|
||
can_manage_settings: false,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
},
|
||
];
|
||
|
||
const createdRoles = [];
|
||
const existingRoles = [];
|
||
|
||
for (const roleData of defaultRoles) {
|
||
// Check if role already exists
|
||
const existingRole = await prisma.role_permissions.findUnique({
|
||
where: { role: roleData.role },
|
||
});
|
||
|
||
if (existingRole) {
|
||
console.log(`✅ Role '${roleData.role}' already exists in database`);
|
||
existingRoles.push(existingRole);
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info(`Role '${roleData.role}' already exists in database`);
|
||
}
|
||
} else {
|
||
// Create new role permission
|
||
const permission = await prisma.role_permissions.create({
|
||
data: roleData,
|
||
});
|
||
createdRoles.push(permission);
|
||
console.log(`🆕 Created role '${roleData.role}' with permissions`);
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info(`Created role '${roleData.role}' with permissions`);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (createdRoles.length > 0) {
|
||
console.log(
|
||
`🎉 Successfully auto-created ${createdRoles.length} role permissions on startup`,
|
||
);
|
||
console.log("📋 Created roles:");
|
||
createdRoles.forEach((role) => {
|
||
console.log(
|
||
` • ${role.role}: dashboard=${role.can_view_dashboard}, hosts=${role.can_manage_hosts}, packages=${role.can_manage_packages}, users=${role.can_manage_users}, settings=${role.can_manage_settings}`,
|
||
);
|
||
});
|
||
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info(
|
||
`✅ Auto-created ${createdRoles.length} role permissions on startup`,
|
||
);
|
||
}
|
||
} else {
|
||
console.log(
|
||
`✅ All default role permissions already exist (${existingRoles.length} roles verified)`,
|
||
);
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info(
|
||
`All default role permissions already exist (${existingRoles.length} roles verified)`,
|
||
);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error(
|
||
"❌ Failed to check/create role permissions on startup:",
|
||
error.message,
|
||
);
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.error(
|
||
"Failed to check/create role permissions on startup:",
|
||
error.message,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Initialize logger - only if logging is enabled
|
||
const logger =
|
||
process.env.ENABLE_LOGGING === "true"
|
||
? winston.createLogger({
|
||
level: process.env.LOG_LEVEL || "info",
|
||
format: winston.format.combine(
|
||
winston.format.timestamp(),
|
||
winston.format.errors({ stack: true }),
|
||
winston.format.json(),
|
||
),
|
||
transports: [],
|
||
})
|
||
: {
|
||
info: () => {},
|
||
error: () => {},
|
||
warn: () => {},
|
||
debug: () => {},
|
||
};
|
||
|
||
// Configure transports based on PM_LOG_TO_CONSOLE environment variable
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
const logToConsole =
|
||
process.env.PM_LOG_TO_CONSOLE === "1" ||
|
||
process.env.PM_LOG_TO_CONSOLE === "true";
|
||
|
||
if (logToConsole) {
|
||
// Log to stdout/stderr instead of files
|
||
logger.add(
|
||
new winston.transports.Console({
|
||
format: winston.format.combine(
|
||
winston.format.timestamp(),
|
||
winston.format.errors({ stack: true }),
|
||
winston.format.printf(({ timestamp, level, message, stack }) => {
|
||
return `${timestamp} [${level.toUpperCase()}]: ${stack || message}`;
|
||
}),
|
||
),
|
||
stderrLevels: ["error", "warn"],
|
||
}),
|
||
);
|
||
} else {
|
||
// Log to files (default behavior)
|
||
logger.add(
|
||
new winston.transports.File({
|
||
filename: "logs/error.log",
|
||
level: "error",
|
||
}),
|
||
);
|
||
logger.add(new winston.transports.File({ filename: "logs/combined.log" }));
|
||
|
||
// Also add console logging for non-production environments
|
||
if (process.env.NODE_ENV !== "production") {
|
||
logger.add(
|
||
new winston.transports.Console({
|
||
format: winston.format.simple(),
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3001;
|
||
|
||
// Trust proxy (needed when behind reverse proxy) and remove X-Powered-By
|
||
if (process.env.TRUST_PROXY) {
|
||
const trustProxyValue = process.env.TRUST_PROXY;
|
||
|
||
// Parse the trust proxy setting according to Express documentation
|
||
if (trustProxyValue === "true") {
|
||
app.set("trust proxy", true);
|
||
} else if (trustProxyValue === "false") {
|
||
app.set("trust proxy", false);
|
||
} else if (/^\d+$/.test(trustProxyValue)) {
|
||
// If it's a number (hop count)
|
||
app.set("trust proxy", parseInt(trustProxyValue, 10));
|
||
} else {
|
||
// If it contains commas, split into array; otherwise use as single value
|
||
// This handles: IP addresses, subnets, named subnets (loopback, linklocal, uniquelocal)
|
||
app.set(
|
||
"trust proxy",
|
||
trustProxyValue.includes(",")
|
||
? trustProxyValue.split(",").map((s) => s.trim())
|
||
: trustProxyValue,
|
||
);
|
||
}
|
||
} else {
|
||
app.set("trust proxy", 1);
|
||
}
|
||
app.disable("x-powered-by");
|
||
|
||
// Rate limiting with monitoring
|
||
const limiter = rateLimit({
|
||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 15 * 60 * 1000,
|
||
max: parseInt(process.env.RATE_LIMIT_MAX, 10) || 100,
|
||
message: {
|
||
error: "Too many requests from this IP, please try again later.",
|
||
retryAfter: Math.ceil(
|
||
(parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 15 * 60 * 1000) / 1000,
|
||
),
|
||
},
|
||
standardHeaders: true,
|
||
legacyHeaders: false,
|
||
skipSuccessfulRequests: true, // Don't count successful requests
|
||
skipFailedRequests: false, // Count failed requests
|
||
});
|
||
|
||
// Middleware
|
||
// Helmet with stricter defaults (CSP/HSTS only in production)
|
||
app.use(
|
||
helmet({
|
||
contentSecurityPolicy:
|
||
process.env.NODE_ENV === "production"
|
||
? {
|
||
useDefaults: true,
|
||
directives: {
|
||
defaultSrc: ["'self'"],
|
||
scriptSrc: ["'self'"],
|
||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||
imgSrc: ["'self'", "data:"],
|
||
fontSrc: ["'self'", "data:"],
|
||
connectSrc: ["'self'"],
|
||
frameAncestors: ["'none'"],
|
||
objectSrc: ["'none'"],
|
||
},
|
||
}
|
||
: false,
|
||
hsts:
|
||
process.env.ENABLE_HSTS === "true" ||
|
||
process.env.NODE_ENV === "production",
|
||
}),
|
||
);
|
||
|
||
// CORS allowlist from comma-separated env
|
||
const parseOrigins = (val) =>
|
||
(val || "")
|
||
.split(",")
|
||
.map((s) => s.trim())
|
||
.filter(Boolean);
|
||
const allowedOrigins = parseOrigins(
|
||
process.env.CORS_ORIGINS ||
|
||
process.env.CORS_ORIGIN ||
|
||
"http://localhost:3000",
|
||
);
|
||
app.use(
|
||
cors({
|
||
origin: (origin, callback) => {
|
||
// Allow non-browser/SSR tools with no origin
|
||
if (!origin) return callback(null, true);
|
||
if (allowedOrigins.includes(origin)) return callback(null, true);
|
||
return callback(new Error("Not allowed by CORS"));
|
||
},
|
||
credentials: true,
|
||
}),
|
||
);
|
||
app.use(limiter);
|
||
// Reduce body size limits to reasonable defaults
|
||
app.use(express.json({ limit: process.env.JSON_BODY_LIMIT || "5mb" }));
|
||
app.use(
|
||
express.urlencoded({
|
||
extended: true,
|
||
limit: process.env.JSON_BODY_LIMIT || "5mb",
|
||
}),
|
||
);
|
||
|
||
// Request logging - only if logging is enabled
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
app.use((req, _, next) => {
|
||
// Log health check requests at debug level to reduce log spam
|
||
if (req.path === "/health") {
|
||
logger.debug(`${req.method} ${req.path} - ${req.ip}`);
|
||
} else {
|
||
logger.info(`${req.method} ${req.path} - ${req.ip}`);
|
||
}
|
||
next();
|
||
});
|
||
}
|
||
|
||
// Health check endpoint
|
||
app.get("/health", (_req, res) => {
|
||
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
||
});
|
||
|
||
// API routes
|
||
const apiVersion = process.env.API_VERSION || "v1";
|
||
|
||
// Per-route rate limits with monitoring
|
||
const authLimiter = rateLimit({
|
||
windowMs:
|
||
parseInt(process.env.AUTH_RATE_LIMIT_WINDOW_MS, 10) || 10 * 60 * 1000,
|
||
max: parseInt(process.env.AUTH_RATE_LIMIT_MAX, 10) || 20,
|
||
message: {
|
||
error: "Too many authentication requests, please try again later.",
|
||
retryAfter: Math.ceil(
|
||
(parseInt(process.env.AUTH_RATE_LIMIT_WINDOW_MS, 10) || 10 * 60 * 1000) /
|
||
1000,
|
||
),
|
||
},
|
||
standardHeaders: true,
|
||
legacyHeaders: false,
|
||
skipSuccessfulRequests: true,
|
||
});
|
||
const agentLimiter = rateLimit({
|
||
windowMs: parseInt(process.env.AGENT_RATE_LIMIT_WINDOW_MS, 10) || 60 * 1000,
|
||
max: parseInt(process.env.AGENT_RATE_LIMIT_MAX, 10) || 120,
|
||
message: {
|
||
error: "Too many agent requests, please try again later.",
|
||
retryAfter: Math.ceil(
|
||
(parseInt(process.env.AGENT_RATE_LIMIT_WINDOW_MS, 10) || 60 * 1000) /
|
||
1000,
|
||
),
|
||
},
|
||
standardHeaders: true,
|
||
legacyHeaders: false,
|
||
skipSuccessfulRequests: true,
|
||
});
|
||
|
||
app.use(`/api/${apiVersion}/auth`, authLimiter, authRoutes);
|
||
app.use(`/api/${apiVersion}/hosts`, agentLimiter, hostRoutes);
|
||
app.use(`/api/${apiVersion}/host-groups`, hostGroupRoutes);
|
||
app.use(`/api/${apiVersion}/packages`, packageRoutes);
|
||
app.use(`/api/${apiVersion}/dashboard`, dashboardRoutes);
|
||
app.use(`/api/${apiVersion}/permissions`, permissionsRoutes);
|
||
app.use(`/api/${apiVersion}/settings`, settingsRoutes);
|
||
app.use(`/api/${apiVersion}/dashboard-preferences`, dashboardPreferencesRoutes);
|
||
app.use(`/api/${apiVersion}/repositories`, repositoryRoutes);
|
||
app.use(`/api/${apiVersion}/version`, versionRoutes);
|
||
app.use(`/api/${apiVersion}/tfa`, tfaRoutes);
|
||
|
||
// Error handling middleware
|
||
app.use((err, _req, res, _next) => {
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.error(err.stack);
|
||
}
|
||
res.status(500).json({
|
||
error: "Something went wrong!",
|
||
message: process.env.NODE_ENV === "development" ? err.message : undefined,
|
||
});
|
||
});
|
||
|
||
// 404 handler
|
||
app.use("*", (_req, res) => {
|
||
res.status(404).json({ error: "Route not found" });
|
||
});
|
||
|
||
// Graceful shutdown
|
||
process.on("SIGINT", async () => {
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info("SIGINT received, shutting down gracefully");
|
||
}
|
||
updateScheduler.stop();
|
||
await disconnectPrisma(prisma);
|
||
process.exit(0);
|
||
});
|
||
|
||
process.on("SIGTERM", async () => {
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info("SIGTERM received, shutting down gracefully");
|
||
}
|
||
updateScheduler.stop();
|
||
await disconnectPrisma(prisma);
|
||
process.exit(0);
|
||
});
|
||
|
||
// Initialize dashboard preferences for all users
|
||
async function initializeDashboardPreferences() {
|
||
try {
|
||
console.log("🔧 Initializing dashboard preferences for all users...");
|
||
|
||
// Get all users
|
||
const users = await prisma.users.findMany({
|
||
select: {
|
||
id: true,
|
||
username: true,
|
||
email: true,
|
||
role: true,
|
||
dashboard_preferences: {
|
||
select: {
|
||
card_id: true,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
if (users.length === 0) {
|
||
console.log("ℹ️ No users found in database");
|
||
return;
|
||
}
|
||
|
||
console.log(`📊 Found ${users.length} users to initialize`);
|
||
|
||
let initializedCount = 0;
|
||
let updatedCount = 0;
|
||
|
||
for (const user of users) {
|
||
const hasPreferences = user.dashboard_preferences.length > 0;
|
||
|
||
// Get permission-based preferences for this user's role
|
||
const expectedPreferences = await getPermissionBasedPreferences(
|
||
user.role,
|
||
);
|
||
const expectedCardCount = expectedPreferences.length;
|
||
|
||
if (!hasPreferences) {
|
||
// User has no preferences - create them
|
||
console.log(
|
||
`⚙️ Creating preferences for ${user.username} (${user.role})`,
|
||
);
|
||
|
||
const preferencesData = expectedPreferences.map((pref) => ({
|
||
id: require("uuid").v4(),
|
||
user_id: user.id,
|
||
card_id: pref.cardId,
|
||
enabled: pref.enabled,
|
||
order: pref.order,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
}));
|
||
|
||
await prisma.dashboard_preferences.createMany({
|
||
data: preferencesData,
|
||
});
|
||
|
||
initializedCount++;
|
||
console.log(
|
||
` ✅ Created ${expectedCardCount} cards based on permissions`,
|
||
);
|
||
} else {
|
||
// User already has preferences - check if they need updating
|
||
const currentCardCount = user.dashboard_preferences.length;
|
||
|
||
if (currentCardCount !== expectedCardCount) {
|
||
console.log(
|
||
`🔄 Updating preferences for ${user.username} (${user.role}) - ${currentCardCount} → ${expectedCardCount} cards`,
|
||
);
|
||
|
||
// Delete existing preferences
|
||
await prisma.dashboard_preferences.deleteMany({
|
||
where: { user_id: user.id },
|
||
});
|
||
|
||
// Create new preferences based on permissions
|
||
const preferencesData = expectedPreferences.map((pref) => ({
|
||
id: require("uuid").v4(),
|
||
user_id: user.id,
|
||
card_id: pref.cardId,
|
||
enabled: pref.enabled,
|
||
order: pref.order,
|
||
created_at: new Date(),
|
||
updated_at: new Date(),
|
||
}));
|
||
|
||
await prisma.dashboard_preferences.createMany({
|
||
data: preferencesData,
|
||
});
|
||
|
||
updatedCount++;
|
||
console.log(
|
||
` ✅ Updated to ${expectedCardCount} cards based on permissions`,
|
||
);
|
||
} else {
|
||
console.log(
|
||
`✅ ${user.username} already has correct preferences (${currentCardCount} cards)`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(`\n📋 Dashboard Preferences Initialization Complete:`);
|
||
console.log(` - New users initialized: ${initializedCount}`);
|
||
console.log(` - Existing users updated: ${updatedCount}`);
|
||
console.log(
|
||
` - Users with correct preferences: ${users.length - initializedCount - updatedCount}`,
|
||
);
|
||
console.log(`\n🎯 Permission-based preferences:`);
|
||
console.log(` - Cards are now assigned based on actual user permissions`);
|
||
console.log(
|
||
` - Each card requires specific permissions (can_view_hosts, can_view_users, etc.)`,
|
||
);
|
||
console.log(` - Users only see cards they have permission to access`);
|
||
} catch (error) {
|
||
console.error("❌ Error initializing dashboard preferences:", error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Helper function to get user permissions based on role
|
||
async function getUserPermissions(userRole) {
|
||
try {
|
||
const permissions = await prisma.role_permissions.findUnique({
|
||
where: { role: userRole },
|
||
});
|
||
|
||
// If no specific permissions found, return default admin permissions (for backward compatibility)
|
||
if (!permissions) {
|
||
console.warn(
|
||
`No permissions found for role: ${userRole}, defaulting to admin access`,
|
||
);
|
||
return {
|
||
can_view_dashboard: true,
|
||
can_view_hosts: true,
|
||
can_manage_hosts: true,
|
||
can_view_packages: true,
|
||
can_manage_packages: true,
|
||
can_view_users: true,
|
||
can_manage_users: true,
|
||
can_view_reports: true,
|
||
can_export_data: true,
|
||
can_manage_settings: true,
|
||
};
|
||
}
|
||
|
||
return permissions;
|
||
} catch (error) {
|
||
console.error("Error fetching user permissions:", error);
|
||
// Return admin permissions as fallback
|
||
return {
|
||
can_view_dashboard: true,
|
||
can_view_hosts: true,
|
||
can_manage_hosts: true,
|
||
can_view_packages: true,
|
||
can_manage_packages: true,
|
||
can_view_users: true,
|
||
can_manage_users: true,
|
||
can_view_reports: true,
|
||
can_export_data: true,
|
||
can_manage_settings: true,
|
||
};
|
||
}
|
||
}
|
||
|
||
// Helper function to get permission-based dashboard preferences for a role
|
||
async function getPermissionBasedPreferences(userRole) {
|
||
// Get user's actual permissions
|
||
const permissions = await getUserPermissions(userRole);
|
||
|
||
// Define all possible dashboard cards with their required permissions
|
||
const allCards = [
|
||
// Host-related cards
|
||
{ cardId: "totalHosts", requiredPermission: "can_view_hosts", order: 0 },
|
||
{
|
||
cardId: "hostsNeedingUpdates",
|
||
requiredPermission: "can_view_hosts",
|
||
order: 1,
|
||
},
|
||
|
||
// Package-related cards
|
||
{
|
||
cardId: "totalOutdatedPackages",
|
||
requiredPermission: "can_view_packages",
|
||
order: 2,
|
||
},
|
||
{
|
||
cardId: "securityUpdates",
|
||
requiredPermission: "can_view_packages",
|
||
order: 3,
|
||
},
|
||
|
||
// Host-related cards (continued)
|
||
{
|
||
cardId: "totalHostGroups",
|
||
requiredPermission: "can_view_hosts",
|
||
order: 4,
|
||
},
|
||
{ cardId: "upToDateHosts", requiredPermission: "can_view_hosts", order: 5 },
|
||
|
||
// Repository-related cards
|
||
{ cardId: "totalRepos", requiredPermission: "can_view_hosts", order: 6 }, // Repos are host-related
|
||
|
||
// User management cards (admin only)
|
||
{ cardId: "totalUsers", requiredPermission: "can_view_users", order: 7 },
|
||
|
||
// System/Report cards
|
||
{
|
||
cardId: "osDistribution",
|
||
requiredPermission: "can_view_reports",
|
||
order: 8,
|
||
},
|
||
{
|
||
cardId: "osDistributionBar",
|
||
requiredPermission: "can_view_reports",
|
||
order: 9,
|
||
},
|
||
{
|
||
cardId: "recentCollection",
|
||
requiredPermission: "can_view_hosts",
|
||
order: 10,
|
||
}, // Collection is host-related
|
||
{
|
||
cardId: "updateStatus",
|
||
requiredPermission: "can_view_reports",
|
||
order: 11,
|
||
},
|
||
{
|
||
cardId: "packagePriority",
|
||
requiredPermission: "can_view_packages",
|
||
order: 12,
|
||
},
|
||
{ cardId: "recentUsers", requiredPermission: "can_view_users", order: 13 },
|
||
{
|
||
cardId: "quickStats",
|
||
requiredPermission: "can_view_dashboard",
|
||
order: 14,
|
||
},
|
||
];
|
||
|
||
// Filter cards based on user's permissions
|
||
const allowedCards = allCards.filter((card) => {
|
||
return permissions[card.requiredPermission] === true;
|
||
});
|
||
|
||
return allowedCards.map((card) => ({
|
||
cardId: card.cardId,
|
||
enabled: true,
|
||
order: card.order, // Preserve original order from allCards
|
||
}));
|
||
}
|
||
|
||
// Start server with database health check
|
||
async function startServer() {
|
||
try {
|
||
// Wait for database to be available
|
||
await waitForDatabase(prisma);
|
||
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info("✅ Database connection successful");
|
||
}
|
||
|
||
// Initialise settings on startup
|
||
try {
|
||
await initSettings();
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info("✅ Settings initialised");
|
||
}
|
||
} catch (initError) {
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.error("❌ Failed to initialise settings:", initError.message);
|
||
}
|
||
throw initError; // Fail startup if settings can't be initialised
|
||
}
|
||
|
||
// Check and import agent version on startup
|
||
await checkAndImportAgentVersion();
|
||
|
||
// Check and create default role permissions on startup
|
||
await checkAndCreateRolePermissions();
|
||
|
||
// Initialize dashboard preferences for all users
|
||
await initializeDashboardPreferences();
|
||
app.listen(PORT, () => {
|
||
if (process.env.ENABLE_LOGGING === "true") {
|
||
logger.info(`Server running on port ${PORT}`);
|
||
logger.info(`Environment: ${process.env.NODE_ENV}`);
|
||
}
|
||
|
||
// Start update scheduler
|
||
updateScheduler.start();
|
||
});
|
||
} catch (error) {
|
||
console.error("❌ Failed to start server:", error.message);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
startServer();
|
||
|
||
module.exports = app;
|