From 5d8a1e71d648659a641e281f90c538559407fcf6 Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Wed, 1 Oct 2025 08:38:40 +0100 Subject: [PATCH] Made changes to the host details area to add notes Reconfigured JWT session timeouts --- backend/env.example | 6 + .../migration.sql | 3 + .../add_user_sessions/migration.sql | 31 + backend/prisma/schema.prisma | 20 + backend/src/middleware/auth.js | 49 +- backend/src/routes/authRoutes.js | 136 +++- backend/src/routes/dashboardRoutes.js | 1 + backend/src/routes/hostRoutes.js | 75 +++ backend/src/server.js | 27 + backend/src/utils/session_manager.js | 319 +++++++++ frontend/src/pages/HostDetail.jsx | 606 ++++++++++-------- frontend/src/pages/Hosts.jsx | 26 +- frontend/src/utils/api.js | 4 + 13 files changed, 1004 insertions(+), 299 deletions(-) create mode 100644 backend/prisma/migrations/20250930234123_add_host_notes/migration.sql create mode 100644 backend/prisma/migrations/add_user_sessions/migration.sql create mode 100644 backend/src/utils/session_manager.js diff --git a/backend/env.example b/backend/env.example index e0d713c..1db1fe7 100644 --- a/backend/env.example +++ b/backend/env.example @@ -23,3 +23,9 @@ ENABLE_LOGGING=true # User Registration DEFAULT_USER_ROLE=user + +# JWT Configuration +JWT_SECRET=your-secure-random-secret-key-change-this-in-production +JWT_EXPIRES_IN=1h +JWT_REFRESH_EXPIRES_IN=7d +SESSION_INACTIVITY_TIMEOUT_MINUTES=30 diff --git a/backend/prisma/migrations/20250930234123_add_host_notes/migration.sql b/backend/prisma/migrations/20250930234123_add_host_notes/migration.sql new file mode 100644 index 0000000..3683c64 --- /dev/null +++ b/backend/prisma/migrations/20250930234123_add_host_notes/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "hosts" ADD COLUMN "notes" TEXT; + diff --git a/backend/prisma/migrations/add_user_sessions/migration.sql b/backend/prisma/migrations/add_user_sessions/migration.sql new file mode 100644 index 0000000..04a45a9 --- /dev/null +++ b/backend/prisma/migrations/add_user_sessions/migration.sql @@ -0,0 +1,31 @@ +-- CreateTable +CREATE TABLE "user_sessions" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "refresh_token" TEXT NOT NULL, + "access_token_hash" TEXT, + "ip_address" TEXT, + "user_agent" TEXT, + "last_activity" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expires_at" TIMESTAMP(3) NOT NULL, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "is_revoked" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "user_sessions_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_sessions_refresh_token_key" ON "user_sessions"("refresh_token"); + +-- CreateIndex +CREATE INDEX "user_sessions_user_id_idx" ON "user_sessions"("user_id"); + +-- CreateIndex +CREATE INDEX "user_sessions_refresh_token_idx" ON "user_sessions"("refresh_token"); + +-- CreateIndex +CREATE INDEX "user_sessions_expires_at_idx" ON "user_sessions"("expires_at"); + +-- AddForeignKey +ALTER TABLE "user_sessions" ADD CONSTRAINT "user_sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 662018e..160aa1c 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -86,6 +86,7 @@ model hosts { selinux_status String? swap_size Int? system_uptime String? + notes String? host_packages host_packages[] host_repositories host_repositories[] host_groups host_groups? @relation(fields: [host_group_id], references: [id]) @@ -186,4 +187,23 @@ model users { first_name String? last_name String? dashboard_preferences dashboard_preferences[] + user_sessions user_sessions[] +} + +model user_sessions { + id String @id + user_id String + refresh_token String @unique + access_token_hash String? + ip_address String? + user_agent String? + last_activity DateTime @default(now()) + expires_at DateTime + created_at DateTime @default(now()) + is_revoked Boolean @default(false) + users users @relation(fields: [user_id], references: [id], onDelete: Cascade) + + @@index([user_id]) + @@index([refresh_token]) + @@index([expires_at]) } diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js index e49ae81..d0d82cb 100644 --- a/backend/src/middleware/auth.js +++ b/backend/src/middleware/auth.js @@ -1,9 +1,13 @@ const jwt = require("jsonwebtoken"); const { PrismaClient } = require("@prisma/client"); +const { + validate_session, + update_session_activity, +} = require("../utils/session_manager"); const prisma = new PrismaClient(); -// Middleware to verify JWT token +// Middleware to verify JWT token with session validation const authenticateToken = async (req, res, next) => { try { const authHeader = req.headers.authorization; @@ -19,35 +23,40 @@ const authenticateToken = async (req, res, next) => { process.env.JWT_SECRET || "your-secret-key", ); - // Get user from database - const user = await prisma.users.findUnique({ - where: { id: decoded.userId }, - select: { - id: true, - username: true, - email: true, - role: true, - is_active: true, - last_login: true, - created_at: true, - updated_at: true, - }, - }); + // Validate session and check inactivity timeout + const validation = await validate_session(decoded.sessionId, token); - if (!user || !user.is_active) { - return res.status(401).json({ error: "Invalid or inactive user" }); + if (!validation.valid) { + const error_messages = { + "Session not found": "Session not found", + "Session revoked": "Session has been revoked", + "Session expired": "Session has expired", + "Session inactive": + validation.message || "Session timed out due to inactivity", + "Token mismatch": "Invalid token", + "User inactive": "User account is inactive", + }; + + return res.status(401).json({ + error: error_messages[validation.reason] || "Authentication failed", + reason: validation.reason, + }); } - // Update last login + // Update session activity timestamp + await update_session_activity(decoded.sessionId); + + // Update last login (only on successful authentication) await prisma.users.update({ - where: { id: user.id }, + where: { id: validation.user.id }, data: { last_login: new Date(), updated_at: new Date(), }, }); - req.user = user; + req.user = validation.user; + req.session_id = decoded.sessionId; next(); } catch (error) { if (error.name === "JsonWebTokenError") { diff --git a/backend/src/routes/authRoutes.js b/backend/src/routes/authRoutes.js index bcffc02..9d04c58 100644 --- a/backend/src/routes/authRoutes.js +++ b/backend/src/routes/authRoutes.js @@ -12,6 +12,13 @@ const { v4: uuidv4 } = require("uuid"); const { createDefaultDashboardPreferences, } = require("./dashboardPreferencesRoutes"); +const { + create_session, + refresh_access_token, + revoke_session, + revoke_all_user_sessions, + get_user_sessions, +} = require("../utils/session_manager"); const router = express.Router(); const prisma = new PrismaClient(); @@ -118,12 +125,16 @@ router.post( // Create default dashboard preferences for the new admin user await createDefaultDashboardPreferences(user.id, "admin"); - // Generate token for immediate login - const token = generateToken(user.id); + // Create session for immediate login + const ip_address = req.ip || req.connection.remoteAddress; + const user_agent = req.get("user-agent"); + const session = await create_session(user.id, ip_address, user_agent); res.status(201).json({ message: "Admin user created successfully", - token, + token: session.access_token, + refresh_token: session.refresh_token, + expires_at: session.expires_at, user: { id: user.id, username: user.username, @@ -722,12 +733,16 @@ router.post( }, }); - // Generate token - const token = generateToken(user.id); + // Create session with access and refresh tokens + const ip_address = req.ip || req.connection.remoteAddress; + const user_agent = req.get("user-agent"); + const session = await create_session(user.id, ip_address, user_agent); res.json({ message: "Login successful", - token, + token: session.access_token, + refresh_token: session.refresh_token, + expires_at: session.expires_at, user: { id: user.id, username: user.username, @@ -829,12 +844,16 @@ router.post( data: { last_login: new Date() }, }); - // Generate token - const jwtToken = generateToken(user.id); + // Create session with access and refresh tokens + const ip_address = req.ip || req.connection.remoteAddress; + const user_agent = req.get("user-agent"); + const session = await create_session(user.id, ip_address, user_agent); res.json({ message: "Login successful", - token: jwtToken, + token: session.access_token, + refresh_token: session.refresh_token, + expires_at: session.expires_at, user: { id: user.id, username: user.username, @@ -1001,9 +1020,14 @@ router.put( }, ); -// Logout (client-side token removal) -router.post("/logout", authenticateToken, async (_req, res) => { +// Logout (revoke current session) +router.post("/logout", authenticateToken, async (req, res) => { try { + // Revoke the current session + if (req.session_id) { + await revoke_session(req.session_id); + } + res.json({ message: "Logout successful", }); @@ -1013,4 +1037,94 @@ router.post("/logout", authenticateToken, async (_req, res) => { } }); +// Logout all sessions (revoke all user sessions) +router.post("/logout-all", authenticateToken, async (req, res) => { + try { + await revoke_all_user_sessions(req.user.id); + + res.json({ + message: "All sessions logged out successfully", + }); + } catch (error) { + console.error("Logout all error:", error); + res.status(500).json({ error: "Logout all failed" }); + } +}); + +// Refresh access token using refresh token +router.post( + "/refresh-token", + [body("refresh_token").notEmpty().withMessage("Refresh token is required")], + async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { refresh_token } = req.body; + + const result = await refresh_access_token(refresh_token); + + if (!result.success) { + return res.status(401).json({ error: result.error }); + } + + res.json({ + message: "Token refreshed successfully", + token: result.access_token, + user: { + id: result.user.id, + username: result.user.username, + email: result.user.email, + role: result.user.role, + is_active: result.user.is_active, + }, + }); + } catch (error) { + console.error("Refresh token error:", error); + res.status(500).json({ error: "Token refresh failed" }); + } + }, +); + +// Get user's active sessions +router.get("/sessions", authenticateToken, async (req, res) => { + try { + const sessions = await get_user_sessions(req.user.id); + + res.json({ + sessions: sessions, + }); + } catch (error) { + console.error("Get sessions error:", error); + res.status(500).json({ error: "Failed to fetch sessions" }); + } +}); + +// Revoke a specific session +router.delete("/sessions/:session_id", authenticateToken, async (req, res) => { + try { + const { session_id } = req.params; + + // Verify the session belongs to the user + const session = await prisma.user_sessions.findUnique({ + where: { id: session_id }, + }); + + if (!session || session.user_id !== req.user.id) { + return res.status(404).json({ error: "Session not found" }); + } + + await revoke_session(session_id); + + res.json({ + message: "Session revoked successfully", + }); + } catch (error) { + console.error("Revoke session error:", error); + res.status(500).json({ error: "Failed to revoke session" }); + } +}); + module.exports = router; diff --git a/backend/src/routes/dashboardRoutes.js b/backend/src/routes/dashboardRoutes.js index 48edf2e..fb54447 100644 --- a/backend/src/routes/dashboardRoutes.js +++ b/backend/src/routes/dashboardRoutes.js @@ -194,6 +194,7 @@ router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => { status: true, agent_version: true, auto_update: true, + notes: true, host_groups: { select: { id: true, diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js index 8a36a64..842f286 100644 --- a/backend/src/routes/hostRoutes.js +++ b/backend/src/routes/hostRoutes.js @@ -858,6 +858,7 @@ router.get( auto_update: true, created_at: true, host_group_id: true, + notes: true, host_groups: { select: { id: true, @@ -1491,4 +1492,78 @@ router.patch( }, ); +// Update host notes (admin only) +router.patch( + "/:hostId/notes", + authenticateToken, + requireManageHosts, + [ + body("notes") + .optional() + .isLength({ max: 1000 }) + .withMessage("Notes must be less than 1000 characters"), + ], + async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + const { hostId } = req.params; + const { notes } = req.body; + + // Check if host exists + const existingHost = await prisma.hosts.findUnique({ + where: { id: hostId }, + }); + + if (!existingHost) { + return res.status(404).json({ error: "Host not found" }); + } + + // Update the notes + const updatedHost = await prisma.hosts.update({ + where: { id: hostId }, + data: { + notes: notes || null, + updated_at: new Date(), + }, + select: { + id: true, + friendly_name: true, + hostname: true, + ip: true, + os_type: true, + os_version: true, + architecture: true, + last_update: true, + status: true, + host_group_id: true, + agent_version: true, + auto_update: true, + created_at: true, + updated_at: true, + notes: true, + host_groups: { + select: { + id: true, + name: true, + color: true, + }, + }, + }, + }); + + res.json({ + message: "Notes updated successfully", + host: updatedHost, + }); + } catch (error) { + console.error("Update notes error:", error); + res.status(500).json({ error: "Failed to update notes" }); + } + }, +); + module.exports = router; diff --git a/backend/src/server.js b/backend/src/server.js index 832cefc..78fc180 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -26,6 +26,7 @@ const versionRoutes = require("./routes/versionRoutes"); const tfaRoutes = require("./routes/tfaRoutes"); const updateScheduler = require("./services/updateScheduler"); const { initSettings } = require("./services/settingsService"); +const { cleanup_expired_sessions } = require("./utils/session_manager"); // Initialize Prisma client with optimized connection pooling for multiple instances const prisma = createPrismaClient(); @@ -399,6 +400,9 @@ process.on("SIGINT", async () => { if (process.env.ENABLE_LOGGING === "true") { logger.info("SIGINT received, shutting down gracefully"); } + if (app.locals.session_cleanup_interval) { + clearInterval(app.locals.session_cleanup_interval); + } updateScheduler.stop(); await disconnectPrisma(prisma); process.exit(0); @@ -408,6 +412,9 @@ process.on("SIGTERM", async () => { if (process.env.ENABLE_LOGGING === "true") { logger.info("SIGTERM received, shutting down gracefully"); } + if (app.locals.session_cleanup_interval) { + clearInterval(app.locals.session_cleanup_interval); + } updateScheduler.stop(); await disconnectPrisma(prisma); process.exit(0); @@ -671,15 +678,35 @@ async function startServer() { // Initialize dashboard preferences for all users await initializeDashboardPreferences(); + + // Initial session cleanup + await cleanup_expired_sessions(); + + // Schedule session cleanup every hour + const session_cleanup_interval = setInterval( + async () => { + try { + await cleanup_expired_sessions(); + } catch (error) { + console.error("Session cleanup error:", error); + } + }, + 60 * 60 * 1000, + ); // Every hour + app.listen(PORT, () => { if (process.env.ENABLE_LOGGING === "true") { logger.info(`Server running on port ${PORT}`); logger.info(`Environment: ${process.env.NODE_ENV}`); + logger.info("✅ Session cleanup scheduled (every hour)"); } // Start update scheduler updateScheduler.start(); }); + + // Store interval for cleanup on shutdown + app.locals.session_cleanup_interval = session_cleanup_interval; } catch (error) { console.error("❌ Failed to start server:", error.message); process.exit(1); diff --git a/backend/src/utils/session_manager.js b/backend/src/utils/session_manager.js new file mode 100644 index 0000000..b941ec5 --- /dev/null +++ b/backend/src/utils/session_manager.js @@ -0,0 +1,319 @@ +const jwt = require("jsonwebtoken"); +const crypto = require("crypto"); +const { PrismaClient } = require("@prisma/client"); + +const prisma = new PrismaClient(); + +/** + * Session Manager - Handles secure session management with inactivity timeout + */ + +// Configuration +const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; +const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1h"; +const JWT_REFRESH_EXPIRES_IN = process.env.JWT_REFRESH_EXPIRES_IN || "7d"; +const INACTIVITY_TIMEOUT_MINUTES = parseInt( + process.env.SESSION_INACTIVITY_TIMEOUT_MINUTES || "30", + 10, +); + +/** + * Generate access token (short-lived) + */ +function generate_access_token(user_id, session_id) { + return jwt.sign({ userId: user_id, sessionId: session_id }, JWT_SECRET, { + expiresIn: JWT_EXPIRES_IN, + }); +} + +/** + * Generate refresh token (long-lived) + */ +function generate_refresh_token() { + return crypto.randomBytes(64).toString("hex"); +} + +/** + * Hash token for storage + */ +function hash_token(token) { + return crypto.createHash("sha256").update(token).digest("hex"); +} + +/** + * Parse expiration string to Date + */ +function parse_expiration(expiration_string) { + const match = expiration_string.match(/^(\d+)([smhd])$/); + if (!match) { + throw new Error("Invalid expiration format"); + } + + const value = parseInt(match[1], 10); + const unit = match[2]; + + const now = new Date(); + switch (unit) { + case "s": + return new Date(now.getTime() + value * 1000); + case "m": + return new Date(now.getTime() + value * 60 * 1000); + case "h": + return new Date(now.getTime() + value * 60 * 60 * 1000); + case "d": + return new Date(now.getTime() + value * 24 * 60 * 60 * 1000); + default: + throw new Error("Invalid time unit"); + } +} + +/** + * Create a new session for user + */ +async function create_session(user_id, ip_address, user_agent) { + try { + const session_id = crypto.randomUUID(); + const refresh_token = generate_refresh_token(); + const access_token = generate_access_token(user_id, session_id); + + const expires_at = parse_expiration(JWT_REFRESH_EXPIRES_IN); + + // Store session in database + await prisma.user_sessions.create({ + data: { + id: session_id, + user_id: user_id, + refresh_token: hash_token(refresh_token), + access_token_hash: hash_token(access_token), + ip_address: ip_address || null, + user_agent: user_agent || null, + last_activity: new Date(), + expires_at: expires_at, + }, + }); + + return { + session_id, + access_token, + refresh_token, + expires_at, + }; + } catch (error) { + console.error("Error creating session:", error); + throw error; + } +} + +/** + * Validate session and check for inactivity timeout + */ +async function validate_session(session_id, access_token) { + try { + const session = await prisma.user_sessions.findUnique({ + where: { id: session_id }, + include: { users: true }, + }); + + if (!session) { + return { valid: false, reason: "Session not found" }; + } + + // Check if session is revoked + if (session.is_revoked) { + return { valid: false, reason: "Session revoked" }; + } + + // Check if session has expired + if (new Date() > session.expires_at) { + await revoke_session(session_id); + return { valid: false, reason: "Session expired" }; + } + + // Check for inactivity timeout + const inactivity_threshold = new Date( + Date.now() - INACTIVITY_TIMEOUT_MINUTES * 60 * 1000, + ); + if (session.last_activity < inactivity_threshold) { + await revoke_session(session_id); + return { + valid: false, + reason: "Session inactive", + message: `Session timed out after ${INACTIVITY_TIMEOUT_MINUTES} minutes of inactivity`, + }; + } + + // Validate access token hash (optional security check) + if (session.access_token_hash) { + const provided_hash = hash_token(access_token); + if (session.access_token_hash !== provided_hash) { + return { valid: false, reason: "Token mismatch" }; + } + } + + // Check if user is still active + if (!session.users.is_active) { + await revoke_session(session_id); + return { valid: false, reason: "User inactive" }; + } + + return { + valid: true, + session, + user: session.users, + }; + } catch (error) { + console.error("Error validating session:", error); + return { valid: false, reason: "Validation error" }; + } +} + +/** + * Update session activity timestamp + */ +async function update_session_activity(session_id) { + try { + await prisma.user_sessions.update({ + where: { id: session_id }, + data: { last_activity: new Date() }, + }); + return true; + } catch (error) { + console.error("Error updating session activity:", error); + return false; + } +} + +/** + * Refresh access token using refresh token + */ +async function refresh_access_token(refresh_token) { + try { + const hashed_token = hash_token(refresh_token); + + const session = await prisma.user_sessions.findUnique({ + where: { refresh_token: hashed_token }, + include: { users: true }, + }); + + if (!session) { + return { success: false, error: "Invalid refresh token" }; + } + + // Validate session + const validation = await validate_session(session.id, ""); + if (!validation.valid) { + return { success: false, error: validation.reason }; + } + + // Generate new access token + const new_access_token = generate_access_token(session.user_id, session.id); + + // Update access token hash + await prisma.user_sessions.update({ + where: { id: session.id }, + data: { + access_token_hash: hash_token(new_access_token), + last_activity: new Date(), + }, + }); + + return { + success: true, + access_token: new_access_token, + user: session.users, + }; + } catch (error) { + console.error("Error refreshing access token:", error); + return { success: false, error: "Token refresh failed" }; + } +} + +/** + * Revoke a session + */ +async function revoke_session(session_id) { + try { + await prisma.user_sessions.update({ + where: { id: session_id }, + data: { is_revoked: true }, + }); + return true; + } catch (error) { + console.error("Error revoking session:", error); + return false; + } +} + +/** + * Revoke all sessions for a user + */ +async function revoke_all_user_sessions(user_id) { + try { + await prisma.user_sessions.updateMany({ + where: { user_id: user_id }, + data: { is_revoked: true }, + }); + return true; + } catch (error) { + console.error("Error revoking user sessions:", error); + return false; + } +} + +/** + * Clean up expired sessions (should be run periodically) + */ +async function cleanup_expired_sessions() { + try { + const result = await prisma.user_sessions.deleteMany({ + where: { + OR: [{ expires_at: { lt: new Date() } }, { is_revoked: true }], + }, + }); + console.log(`Cleaned up ${result.count} expired sessions`); + return result.count; + } catch (error) { + console.error("Error cleaning up sessions:", error); + return 0; + } +} + +/** + * Get active sessions for a user + */ +async function get_user_sessions(user_id) { + try { + return await prisma.user_sessions.findMany({ + where: { + user_id: user_id, + is_revoked: false, + expires_at: { gt: new Date() }, + }, + select: { + id: true, + ip_address: true, + user_agent: true, + last_activity: true, + created_at: true, + expires_at: true, + }, + orderBy: { last_activity: "desc" }, + }); + } catch (error) { + console.error("Error getting user sessions:", error); + return []; + } +} + +module.exports = { + create_session, + validate_session, + update_session_activity, + refresh_access_token, + revoke_session, + revoke_all_user_sessions, + cleanup_expired_sessions, + get_user_sessions, + generate_access_token, + INACTIVITY_TIMEOUT_MINUTES, +}; diff --git a/frontend/src/pages/HostDetail.jsx b/frontend/src/pages/HostDetail.jsx index 6e7a998..8f2b222 100644 --- a/frontend/src/pages/HostDetail.jsx +++ b/frontend/src/pages/HostDetail.jsx @@ -102,6 +102,15 @@ const HostDetail = () => { }, }); + const updateNotesMutation = useMutation({ + mutationFn: ({ hostId, notes }) => + adminHostsAPI.updateNotes(hostId, notes).then((res) => res.data), + onSuccess: () => { + queryClient.invalidateQueries(["host", hostId]); + queryClient.invalidateQueries(["hosts"]); + }, + }); + const handleDeleteHost = async () => { if ( window.confirm( @@ -315,17 +324,6 @@ const HostDetail = () => { > System - + @@ -506,55 +515,279 @@ const HostDetail = () => { )} {/* System Information */} - {activeTab === "system" && - (host.kernel_version || - host.selinux_status || - host.architecture) && ( -
-
- {host.architecture && ( -
-

- Architecture -

-

- {host.architecture} -

-
- )} + {activeTab === "system" && ( +
+ {/* Basic System Information */} + {(host.kernel_version || + host.selinux_status || + host.architecture) && ( +
+

+ + System Information +

+
+ {host.architecture && ( +
+

+ Architecture +

+

+ {host.architecture} +

+
+ )} - {host.kernel_version && ( -
-

- Kernel Version -

-

- {host.kernel_version} -

-
- )} + {host.kernel_version && ( +
+

+ Kernel Version +

+

+ {host.kernel_version} +

+
+ )} - {host.selinux_status && ( -
-

- SELinux Status -

- - {host.selinux_status} - -
- )} + {/* Empty div to push SELinux status to the right */} +
+ + {host.selinux_status && ( +
+

+ SELinux Status +

+ + {host.selinux_status} + +
+ )} +
-
- )} + )} + + {/* Resource Information */} + {(host.system_uptime || + host.cpu_model || + host.cpu_cores || + host.ram_installed || + host.swap_size !== undefined || + (host.load_average && + Array.isArray(host.load_average) && + host.load_average.length > 0 && + host.load_average.some((load) => load != null)) || + (host.disk_details && + Array.isArray(host.disk_details) && + host.disk_details.length > 0)) && ( +
+

+ + Resource Information +

+ + {/* System Overview */} +
+ {/* System Uptime */} + {host.system_uptime && ( +
+
+ +

+ System Uptime +

+
+

+ {host.system_uptime} +

+
+ )} + + {/* CPU Model */} + {host.cpu_model && ( +
+
+ +

+ CPU Model +

+
+

+ {host.cpu_model} +

+
+ )} + + {/* CPU Cores */} + {host.cpu_cores && ( +
+
+ +

+ CPU Cores +

+
+

+ {host.cpu_cores} +

+
+ )} + + {/* RAM Installed */} + {host.ram_installed && ( +
+
+ +

+ RAM Installed +

+
+

+ {host.ram_installed} GB +

+
+ )} + + {/* Swap Size */} + {host.swap_size !== undefined && + host.swap_size !== null && ( +
+
+ +

+ Swap Size +

+
+

+ {host.swap_size} GB +

+
+ )} + + {/* Load Average */} + {host.load_average && + Array.isArray(host.load_average) && + host.load_average.length > 0 && + host.load_average.some((load) => load != null) && ( +
+
+ +

+ Load Average +

+
+

+ {host.load_average + .filter((load) => load != null) + .map((load, index) => ( + + {typeof load === "number" + ? load.toFixed(2) + : String(load)} + {index < + host.load_average.filter( + (load) => load != null, + ).length - + 1 && ", "} + + ))} +

+
+ )} +
+ + {/* Disk Information */} + {host.disk_details && + Array.isArray(host.disk_details) && + host.disk_details.length > 0 && ( +
+
+ + Disk Usage +
+
+ {host.disk_details.map((disk, index) => ( +
+
+ + + {disk.name || `Disk ${index + 1}`} + +
+ {disk.size && ( +

+ Size: {disk.size} +

+ )} + {disk.mountpoint && ( +

+ Mount: {disk.mountpoint} +

+ )} + {disk.usage && + typeof disk.usage === "number" && ( +
+
+ Usage + {disk.usage}% +
+
+
+
+
+ )} +
+ ))} +
+
+ )} +
+ )} + + {/* No Data State */} + {!host.kernel_version && + !host.selinux_status && + !host.architecture && + !host.system_uptime && + !host.cpu_model && + !host.cpu_cores && + !host.ram_installed && + host.swap_size === undefined && + (!host.load_average || + !Array.isArray(host.load_average) || + host.load_average.length === 0 || + !host.load_average.some((load) => load != null)) && + (!host.disk_details || + !Array.isArray(host.disk_details) || + host.disk_details.length === 0) && ( +
+ +

+ No system information available +

+

+ System information will appear once the agent collects + data from this host +

+
+ )} +
+ )} {activeTab === "network" && !( @@ -570,213 +803,6 @@ const HostDetail = () => {
)} - {activeTab === "system" && - !( - host.kernel_version || - host.selinux_status || - host.architecture - ) && ( -
- -

- No system information available -

-
- )} - - {/* System Monitoring */} - {activeTab === "monitoring" && ( -
- {/* System Overview */} -
- {/* System Uptime */} - {host.system_uptime && ( -
-
- -

- System Uptime -

-
-

- {host.system_uptime} -

-
- )} - - {/* CPU Model */} - {host.cpu_model && ( -
-
- -

- CPU Model -

-
-

- {host.cpu_model} -

-
- )} - - {/* CPU Cores */} - {host.cpu_cores && ( -
-
- -

- CPU Cores -

-
-

- {host.cpu_cores} -

-
- )} - - {/* RAM Installed */} - {host.ram_installed && ( -
-
- -

- RAM Installed -

-
-

- {host.ram_installed} GB -

-
- )} - - {/* Swap Size */} - {host.swap_size !== undefined && - host.swap_size !== null && ( -
-
- -

- Swap Size -

-
-

- {host.swap_size} GB -

-
- )} - - {/* Load Average */} - {host.load_average && - Array.isArray(host.load_average) && - host.load_average.length > 0 && - host.load_average.some((load) => load != null) && ( -
-
- -

- Load Average -

-
-

- {host.load_average - .filter((load) => load != null) - .map((load, index) => ( - - {typeof load === "number" - ? load.toFixed(2) - : String(load)} - {index < - host.load_average.filter( - (load) => load != null, - ).length - - 1 && ", "} - - ))} -

-
- )} -
- - {/* Disk Information */} - {host.disk_details && - Array.isArray(host.disk_details) && - host.disk_details.length > 0 && ( -
-

- - Disk Usage -

-
- {host.disk_details.map((disk, index) => ( -
-
- - - {disk.name || `Disk ${index + 1}`} - -
- {disk.size && ( -

- Size: {disk.size} -

- )} - {disk.mountpoint && ( -

- Mount: {disk.mountpoint} -

- )} - {disk.usage && typeof disk.usage === "number" && ( -
-
- Usage - {disk.usage}% -
-
-
-
-
- )} -
- ))} -
-
- )} - - {/* No Data State */} - {!host.system_uptime && - !host.cpu_model && - !host.cpu_cores && - !host.ram_installed && - host.swap_size === undefined && - (!host.load_average || - !Array.isArray(host.load_average) || - host.load_average.length === 0 || - !host.load_average.some((load) => load != null)) && - (!host.disk_details || - !Array.isArray(host.disk_details) || - host.disk_details.length === 0) && ( -
- -

- No monitoring data available -

-

- Monitoring data will appear once the agent collects - system information -

-
- )} -
- )} - {/* Update History */} {activeTab === "history" && (
@@ -883,6 +909,56 @@ const HostDetail = () => { )}
)} + + {/* Notes */} + {activeTab === "notes" && ( +
+
+

+ Host Notes +

+
+
+